vg-x07df 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.
Files changed (140) hide show
  1. package/.azure-pipelines/publish-public.yml +37 -0
  2. package/.azure-pipelines/publish.yml +39 -0
  3. package/.changeset/README.md +8 -0
  4. package/.changeset/config.json +11 -0
  5. package/AUTO_JOIN_GUIDE.md +411 -0
  6. package/README.md +215 -0
  7. package/Screenshot 2025-09-24 at 14.34.48.png +0 -0
  8. package/Screenshot 2025-10-04 at 12.58.54.png +0 -0
  9. package/biome.json +48 -0
  10. package/examples/demo/.env.example +19 -0
  11. package/examples/demo/CHANGELOG.md +22 -0
  12. package/examples/demo/README.md +72 -0
  13. package/examples/demo/eslint.config.js +23 -0
  14. package/examples/demo/index.html +13 -0
  15. package/examples/demo/package.json +34 -0
  16. package/examples/demo/pnpm-lock.yaml +2098 -0
  17. package/examples/demo/pnpm-workspace.yaml +1 -0
  18. package/examples/demo/public/vite.svg +1 -0
  19. package/examples/demo/src/App.css +52 -0
  20. package/examples/demo/src/App.tsx +176 -0
  21. package/examples/demo/src/assets/react.svg +1 -0
  22. package/examples/demo/src/components/auth/LoginForm.css +144 -0
  23. package/examples/demo/src/components/auth/LoginForm.tsx +80 -0
  24. package/examples/demo/src/components/calling/AutoJoinSettings.tsx +213 -0
  25. package/examples/demo/src/components/calling/AutoJoinStatus.tsx +72 -0
  26. package/examples/demo/src/components/calling/CallInitiator.css +258 -0
  27. package/examples/demo/src/components/calling/CallInitiator.tsx +142 -0
  28. package/examples/demo/src/components/calling/CallNotifications.css +119 -0
  29. package/examples/demo/src/components/calling/CallNotifications.tsx +108 -0
  30. package/examples/demo/src/components/calling/IncomingCallModal.css +192 -0
  31. package/examples/demo/src/components/calling/IncomingCallModal.tsx +78 -0
  32. package/examples/demo/src/components/calling/MinimizedCall.css +156 -0
  33. package/examples/demo/src/components/calling/MinimizedCall.tsx +78 -0
  34. package/examples/demo/src/components/conference/ConferenceHeader.css +265 -0
  35. package/examples/demo/src/components/conference/ConferenceHeader.tsx +78 -0
  36. package/examples/demo/src/components/conference/EnhancedControlBar.css +356 -0
  37. package/examples/demo/src/components/conference/EnhancedControlBar.tsx +262 -0
  38. package/examples/demo/src/components/conference/PaginationControls.css +67 -0
  39. package/examples/demo/src/components/conference/PaginationControls.tsx +64 -0
  40. package/examples/demo/src/components/conference/ParticipantGrid.css +153 -0
  41. package/examples/demo/src/components/conference/ParticipantGrid.tsx +87 -0
  42. package/examples/demo/src/components/conference/ParticipantTile.css +210 -0
  43. package/examples/demo/src/components/conference/ParticipantTile.tsx +114 -0
  44. package/examples/demo/src/components/conference/VideoConference.css +214 -0
  45. package/examples/demo/src/components/conference/VideoConference.tsx +93 -0
  46. package/examples/demo/src/contexts/AuthContext.tsx +105 -0
  47. package/examples/demo/src/hooks/useAuth.ts +5 -0
  48. package/examples/demo/src/hooks/useCallTimer.ts +42 -0
  49. package/examples/demo/src/index.css +68 -0
  50. package/examples/demo/src/main.tsx +10 -0
  51. package/examples/demo/src/services/auth.service.ts +153 -0
  52. package/examples/demo/src/types/auth.types.ts +31 -0
  53. package/examples/demo/tsconfig.app.json +28 -0
  54. package/examples/demo/tsconfig.json +7 -0
  55. package/examples/demo/tsconfig.node.json +26 -0
  56. package/examples/demo/vite.config.ts +15 -0
  57. package/images/callpad-without-ai.png +0 -0
  58. package/package.json +28 -0
  59. package/packages/sdk/CHANGELOG.md +33 -0
  60. package/packages/sdk/LICENSE +21 -0
  61. package/packages/sdk/README.md +97 -0
  62. package/packages/sdk/documentation.md +1132 -0
  63. package/packages/sdk/openapi-ts.config.ts +7 -0
  64. package/packages/sdk/package.json +88 -0
  65. package/packages/sdk/src/core/auth.manager.ts +52 -0
  66. package/packages/sdk/src/core/events/event-bus.ts +301 -0
  67. package/packages/sdk/src/core/events/index.ts +8 -0
  68. package/packages/sdk/src/core/events/types.ts +165 -0
  69. package/packages/sdk/src/core/index.ts +3 -0
  70. package/packages/sdk/src/core/signal/api.config.ts +49 -0
  71. package/packages/sdk/src/core/signal/index.ts +16 -0
  72. package/packages/sdk/src/core/signal/signal.client.ts +101 -0
  73. package/packages/sdk/src/core/signal/types.ts +110 -0
  74. package/packages/sdk/src/core/socketio/handlers/base.handler.ts +212 -0
  75. package/packages/sdk/src/core/socketio/handlers/call-accepted.handler.ts +34 -0
  76. package/packages/sdk/src/core/socketio/handlers/call-canceled.handler.ts +34 -0
  77. package/packages/sdk/src/core/socketio/handlers/call-declined.handler.ts +29 -0
  78. package/packages/sdk/src/core/socketio/handlers/call-ended.handler.ts +40 -0
  79. package/packages/sdk/src/core/socketio/handlers/call-incoming.handler.ts +72 -0
  80. package/packages/sdk/src/core/socketio/handlers/call-join-info.handler.ts +181 -0
  81. package/packages/sdk/src/core/socketio/handlers/call-participant-joined.handler.ts +42 -0
  82. package/packages/sdk/src/core/socketio/handlers/call-participant-joining.handler.ts +42 -0
  83. package/packages/sdk/src/core/socketio/handlers/call-timeout.handler.ts +31 -0
  84. package/packages/sdk/src/core/socketio/handlers/handler.registry.ts +62 -0
  85. package/packages/sdk/src/core/socketio/handlers/index.ts +21 -0
  86. package/packages/sdk/src/core/socketio/handlers/participant-left.handler.ts +37 -0
  87. package/packages/sdk/src/core/socketio/handlers/schema.ts +130 -0
  88. package/packages/sdk/src/core/socketio/index.ts +5 -0
  89. package/packages/sdk/src/core/socketio/socket.manager.ts +187 -0
  90. package/packages/sdk/src/core/socketio/types.ts +14 -0
  91. package/packages/sdk/src/core/types.ts +23 -0
  92. package/packages/sdk/src/generated/api/core/ApiError.ts +21 -0
  93. package/packages/sdk/src/generated/api/core/ApiRequestOptions.ts +13 -0
  94. package/packages/sdk/src/generated/api/core/ApiResult.ts +7 -0
  95. package/packages/sdk/src/generated/api/core/CancelablePromise.ts +126 -0
  96. package/packages/sdk/src/generated/api/core/OpenAPI.ts +55 -0
  97. package/packages/sdk/src/generated/api/core/request.ts +339 -0
  98. package/packages/sdk/src/generated/api/index.ts +5 -0
  99. package/packages/sdk/src/generated/api/models.ts +219 -0
  100. package/packages/sdk/src/generated/api/services.ts +225 -0
  101. package/packages/sdk/src/hooks/index.ts +21 -0
  102. package/packages/sdk/src/hooks/useAutoJoin.ts +66 -0
  103. package/packages/sdk/src/hooks/useCallActions.ts +28 -0
  104. package/packages/sdk/src/hooks/useCallQuality.ts +416 -0
  105. package/packages/sdk/src/hooks/useCallState.ts +23 -0
  106. package/packages/sdk/src/hooks/useConnection.ts +15 -0
  107. package/packages/sdk/src/hooks/useDevices.ts +296 -0
  108. package/packages/sdk/src/hooks/useErrorRecovery.ts +299 -0
  109. package/packages/sdk/src/hooks/useErrors.ts +84 -0
  110. package/packages/sdk/src/hooks/useEvent.ts +188 -0
  111. package/packages/sdk/src/hooks/useMediaControls.ts +215 -0
  112. package/packages/sdk/src/hooks/useParticipantStatus.ts +318 -0
  113. package/packages/sdk/src/hooks/useParticipants.ts +111 -0
  114. package/packages/sdk/src/index.ts +66 -0
  115. package/packages/sdk/src/livekit/constants.ts +76 -0
  116. package/packages/sdk/src/livekit/device.manager.ts +172 -0
  117. package/packages/sdk/src/livekit/error-classifier.ts +155 -0
  118. package/packages/sdk/src/livekit/events/eventBridge.ts +371 -0
  119. package/packages/sdk/src/livekit/events/trackRegistry.ts +114 -0
  120. package/packages/sdk/src/livekit/index.ts +49 -0
  121. package/packages/sdk/src/livekit/livekit.service.ts +110 -0
  122. package/packages/sdk/src/livekit/media.controls.ts +315 -0
  123. package/packages/sdk/src/livekit/room.manager.ts +79 -0
  124. package/packages/sdk/src/livekit/track.utils.ts +230 -0
  125. package/packages/sdk/src/livekit/types.ts +135 -0
  126. package/packages/sdk/src/provider/RtcProvider.tsx +78 -0
  127. package/packages/sdk/src/services/call-actions.ts +260 -0
  128. package/packages/sdk/src/services/error-recovery.ts +461 -0
  129. package/packages/sdk/src/services/index.ts +2 -0
  130. package/packages/sdk/src/services/sdk-builder.ts +104 -0
  131. package/packages/sdk/src/state/errors.ts +163 -0
  132. package/packages/sdk/src/state/selectors.ts +28 -0
  133. package/packages/sdk/src/state/store.ts +36 -0
  134. package/packages/sdk/src/state/types.ts +151 -0
  135. package/packages/sdk/src/utils/logger.ts +183 -0
  136. package/packages/sdk/tsconfig.json +49 -0
  137. package/packages/sdk/tsup.config.ts +51 -0
  138. package/pnpm-workspace.yaml +4 -0
  139. package/tsconfig.base.json +19 -0
  140. package/turbo.json +34 -0
@@ -0,0 +1,262 @@
1
+ import { useState } from 'react';
2
+ import {
3
+ useMediaControls,
4
+ useDevices,
5
+ useParticipants,
6
+ useCallActions,
7
+ useConnection,
8
+ useCallState
9
+ } from 'vg-x07df';
10
+ import { useCallTimer } from '../../hooks/useCallTimer';
11
+ import './EnhancedControlBar.css';
12
+
13
+ interface EnhancedControlBarProps {
14
+ onLeaveCall?: () => void;
15
+ }
16
+
17
+ export function EnhancedControlBar({ onLeaveCall }: EnhancedControlBarProps) {
18
+ const [showDevices, setShowDevices] = useState(false);
19
+ const [showParticipants, setShowParticipants] = useState(false);
20
+
21
+ const {
22
+ isAudioEnabled,
23
+ isVideoEnabled,
24
+ toggleMicrophone,
25
+ toggleCamera,
26
+ isLoading: mediaLoading
27
+ } = useMediaControls();
28
+
29
+ const {
30
+ mics,
31
+ cams,
32
+ speakers,
33
+ switchMicrophone,
34
+ switchCamera,
35
+ switchSpeaker
36
+ } = useDevices();
37
+
38
+ const { participants } = useParticipants();
39
+ const { end } = useCallActions();
40
+ const { quality } = useConnection();
41
+ const { formattedDuration } = useCallTimer();
42
+
43
+ // Get current call ID from SDK state
44
+ const { id: callId } = useCallState();
45
+
46
+ const handleLeaveCall = async () => {
47
+ if (!callId) {
48
+ console.error('No active call to leave');
49
+ return;
50
+ }
51
+
52
+ try {
53
+ await end(callId);
54
+ onLeaveCall?.();
55
+ } catch (error) {
56
+ console.error('Failed to leave call:', error);
57
+ }
58
+ };
59
+
60
+ const participantCount = participants.length;
61
+
62
+ return (
63
+ <div className="enhanced-control-bar">
64
+ {/* Left section - Call info */}
65
+ <div className="control-section left">
66
+ <div className="call-info">
67
+ <span className="call-duration">{formattedDuration}</span>
68
+ {quality && (
69
+ <div className={`connection-indicator ${quality.toLowerCase()}`}>
70
+ <div className="signal-dots">
71
+ <div className="dot"></div>
72
+ <div className="dot"></div>
73
+ <div className="dot"></div>
74
+ </div>
75
+ </div>
76
+ )}
77
+ </div>
78
+ </div>
79
+
80
+ {/* Center section - Media controls */}
81
+ <div className="control-section center">
82
+ <div className="media-controls">
83
+ {/* Microphone control */}
84
+ <div className="control-group">
85
+ <button
86
+ onClick={toggleMicrophone}
87
+ disabled={mediaLoading}
88
+ className={`control-button microphone ${!isAudioEnabled ? 'disabled' : ''}`}
89
+ title={isAudioEnabled ? 'Mute microphone' : 'Unmute microphone'}
90
+ >
91
+ <span className="icon">
92
+ {isAudioEnabled ? '🎤' : '🔇'}
93
+ </span>
94
+ </button>
95
+
96
+ {mics.length > 1 && (
97
+ <button
98
+ className="device-selector-trigger"
99
+ onClick={() => setShowDevices(!showDevices)}
100
+ title="Select microphone"
101
+ >
102
+
103
+ </button>
104
+ )}
105
+ </div>
106
+
107
+ {/* Camera control */}
108
+ <div className="control-group">
109
+ <button
110
+ onClick={toggleCamera}
111
+ disabled={mediaLoading}
112
+ className={`control-button camera ${!isVideoEnabled ? 'disabled' : ''}`}
113
+ title={isVideoEnabled ? 'Turn off camera' : 'Turn on camera'}
114
+ >
115
+ <span className="icon">
116
+ {isVideoEnabled ? '📹' : '📷'}
117
+ </span>
118
+ </button>
119
+
120
+ {cams.length > 1 && (
121
+ <button
122
+ className="device-selector-trigger"
123
+ onClick={() => setShowDevices(!showDevices)}
124
+ title="Select camera"
125
+ >
126
+
127
+ </button>
128
+ )}
129
+ </div>
130
+
131
+ {/* Screen share control */}
132
+ <button
133
+ className="control-button screen-share"
134
+ title="Share screen"
135
+ onClick={() => {
136
+ // TODO: Implement screen sharing
137
+ console.log('Screen share clicked');
138
+ }}
139
+ >
140
+ <span className="icon">🖥️</span>
141
+ </button>
142
+
143
+ {/* Participants panel */}
144
+ <button
145
+ className={`control-button participants ${showParticipants ? 'active' : ''}`}
146
+ onClick={() => setShowParticipants(!showParticipants)}
147
+ title="Show participants"
148
+ >
149
+ <span className="icon">👥</span>
150
+ {participantCount > 0 && (
151
+ <span className="participant-count">{participantCount}</span>
152
+ )}
153
+ </button>
154
+
155
+ {/* Chat control */}
156
+ <button
157
+ className="control-button chat"
158
+ title="Open chat"
159
+ onClick={() => {
160
+ // TODO: Implement chat
161
+ console.log('Chat clicked');
162
+ }}
163
+ >
164
+ <span className="icon">💬</span>
165
+ </button>
166
+
167
+ {/* More options */}
168
+ <button
169
+ className={`control-button more ${showDevices ? 'active' : ''}`}
170
+ onClick={() => setShowDevices(!showDevices)}
171
+ title="More options"
172
+ >
173
+ <span className="icon">⚙️</span>
174
+ </button>
175
+ </div>
176
+ </div>
177
+
178
+ {/* Right section - Call actions */}
179
+ <div className="control-section right">
180
+ <button
181
+ onClick={handleLeaveCall}
182
+ className="control-button leave-call"
183
+ title="Leave call"
184
+ >
185
+ <span className="icon">📞</span>
186
+ </button>
187
+ </div>
188
+
189
+ {/* Device selector dropdown */}
190
+ {showDevices && (
191
+ <div className="device-selector-panel">
192
+ <div className="device-section">
193
+ <h4>Microphone</h4>
194
+ {mics.map((mic: any) => (
195
+ <button
196
+ key={mic.deviceId}
197
+ onClick={() => switchMicrophone(mic.deviceId)}
198
+ className="device-option"
199
+ >
200
+ {mic.label || `Microphone ${mic.deviceId.slice(0, 8)}`}
201
+ </button>
202
+ ))}
203
+ </div>
204
+
205
+ <div className="device-section">
206
+ <h4>Camera</h4>
207
+ {cams.map((cam: any) => (
208
+ <button
209
+ key={cam.deviceId}
210
+ onClick={() => switchCamera(cam.deviceId)}
211
+ className="device-option"
212
+ >
213
+ {cam.label || `Camera ${cam.deviceId.slice(0, 8)}`}
214
+ </button>
215
+ ))}
216
+ </div>
217
+
218
+ <div className="device-section">
219
+ <h4>Speaker</h4>
220
+ {speakers.map((speaker: any) => (
221
+ <button
222
+ key={speaker.deviceId}
223
+ onClick={() => switchSpeaker(speaker.deviceId)}
224
+ className="device-option"
225
+ >
226
+ {speaker.label || `Speaker ${speaker.deviceId.slice(0, 8)}`}
227
+ </button>
228
+ ))}
229
+ </div>
230
+ </div>
231
+ )}
232
+
233
+ {/* Participants panel */}
234
+ {showParticipants && (
235
+ <div className="participants-panel">
236
+ <h4>Participants ({participantCount})</h4>
237
+ <div className="participants-list">
238
+ {participants.map((participant: any) => (
239
+ <div key={participant.id} className="participant-item">
240
+ <div className="participant-avatar">
241
+ {participant.avatarUrl ? (
242
+ <img src={participant.avatarUrl} alt="" />
243
+ ) : (
244
+ <span>
245
+ {(participant.firstName || 'U').charAt(0).toUpperCase()}
246
+ </span>
247
+ )}
248
+ </div>
249
+ <span className="participant-name">
250
+ {[participant.firstName, participant.lastName].filter(Boolean).join(' ') || `User ${participant.id}`}
251
+ </span>
252
+ {participant.isSpeaking && (
253
+ <span className="speaking-indicator">🗣️</span>
254
+ )}
255
+ </div>
256
+ ))}
257
+ </div>
258
+ </div>
259
+ )}
260
+ </div>
261
+ );
262
+ }
@@ -0,0 +1,67 @@
1
+ .pagination-controls {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ gap: 12px;
6
+ padding: 12px 0;
7
+ margin-top: 8px;
8
+ }
9
+
10
+ .pagination-button {
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ width: 32px;
15
+ height: 32px;
16
+ border: 1px solid rgba(255, 255, 255, 0.2);
17
+ background: rgba(0, 0, 0, 0.3);
18
+ color: white;
19
+ border-radius: 6px;
20
+ cursor: pointer;
21
+ font-size: 14px;
22
+ transition: all 0.2s ease;
23
+ backdrop-filter: blur(8px);
24
+ }
25
+
26
+ .pagination-button:hover:not(:disabled) {
27
+ background: rgba(255, 255, 255, 0.1);
28
+ border-color: rgba(255, 255, 255, 0.3);
29
+ transform: translateY(-1px);
30
+ }
31
+
32
+ .pagination-button:active:not(:disabled) {
33
+ transform: translateY(0);
34
+ }
35
+
36
+ .pagination-button:disabled {
37
+ opacity: 0.4;
38
+ cursor: not-allowed;
39
+ }
40
+
41
+ .page-indicator {
42
+ font-size: 14px;
43
+ font-weight: 500;
44
+ color: rgba(255, 255, 255, 0.9);
45
+ min-width: 40px;
46
+ text-align: center;
47
+ user-select: none;
48
+ }
49
+
50
+ /* Match the existing UI theme */
51
+ @media (max-width: 768px) {
52
+ .pagination-controls {
53
+ gap: 8px;
54
+ padding: 8px 0;
55
+ }
56
+
57
+ .pagination-button {
58
+ width: 28px;
59
+ height: 28px;
60
+ font-size: 12px;
61
+ }
62
+
63
+ .page-indicator {
64
+ font-size: 13px;
65
+ min-width: 35px;
66
+ }
67
+ }
@@ -0,0 +1,64 @@
1
+ import { useEffect } from 'react';
2
+ import './PaginationControls.css';
3
+
4
+ interface PaginationControlsProps {
5
+ currentPage: number;
6
+ totalPages: number;
7
+ onPreviousPage: () => void;
8
+ onNextPage: () => void;
9
+ hasNextPage: boolean;
10
+ hasPreviousPage: boolean;
11
+ }
12
+
13
+ export function PaginationControls({
14
+ currentPage,
15
+ totalPages,
16
+ onPreviousPage,
17
+ onNextPage,
18
+ hasNextPage,
19
+ hasPreviousPage,
20
+ }: PaginationControlsProps) {
21
+ // Keyboard navigation support
22
+ useEffect(() => {
23
+ const handleKeyDown = (event: KeyboardEvent) => {
24
+ if (event.key === 'ArrowLeft' && hasPreviousPage) {
25
+ onPreviousPage();
26
+ } else if (event.key === 'ArrowRight' && hasNextPage) {
27
+ onNextPage();
28
+ }
29
+ };
30
+
31
+ window.addEventListener('keydown', handleKeyDown);
32
+ return () => window.removeEventListener('keydown', handleKeyDown);
33
+ }, [hasNextPage, hasPreviousPage, onNextPage, onPreviousPage]);
34
+
35
+ if (totalPages <= 1) {
36
+ return null;
37
+ }
38
+
39
+ return (
40
+ <div className="pagination-controls">
41
+ <button
42
+ className="pagination-button prev"
43
+ onClick={onPreviousPage}
44
+ disabled={!hasPreviousPage}
45
+ title="Previous page (←)"
46
+ >
47
+
48
+ </button>
49
+
50
+ <span className="page-indicator">
51
+ {currentPage}/{totalPages}
52
+ </span>
53
+
54
+ <button
55
+ className="pagination-button next"
56
+ onClick={onNextPage}
57
+ disabled={!hasNextPage}
58
+ title="Next page (→)"
59
+ >
60
+
61
+ </button>
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,153 @@
1
+ .participant-grid-container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ width: 100%;
5
+ height: 100%;
6
+ background: #0f172a;
7
+ border-radius: 12px;
8
+ overflow: hidden;
9
+ }
10
+
11
+ .participant-grid {
12
+ display: grid;
13
+ gap: 8px;
14
+ flex: 1;
15
+ padding: 16px 16px 8px 16px;
16
+ }
17
+
18
+ .participant-grid-container.empty {
19
+ justify-content: center;
20
+ align-items: center;
21
+ }
22
+
23
+ .participant-grid-container.empty .participant-grid {
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ padding: 16px;
28
+ }
29
+
30
+ .empty-state {
31
+ text-align: center;
32
+ color: #64748b;
33
+ padding: 40px 20px;
34
+ }
35
+
36
+ .empty-icon {
37
+ font-size: 48px;
38
+ margin-bottom: 16px;
39
+ opacity: 0.7;
40
+ }
41
+
42
+ .empty-state h3 {
43
+ margin: 0 0 8px 0;
44
+ font-size: 18px;
45
+ font-weight: 600;
46
+ color: #94a3b8;
47
+ }
48
+
49
+ .empty-state p {
50
+ margin: 0;
51
+ font-size: 14px;
52
+ color: #64748b;
53
+ }
54
+
55
+ .overflow-indicator {
56
+ display: flex;
57
+ align-items: center;
58
+ justify-content: center;
59
+ background: rgba(0, 0, 0, 0.8);
60
+ border-radius: 8px;
61
+ border: 2px dashed rgba(255, 255, 255, 0.3);
62
+ backdrop-filter: blur(8px);
63
+ }
64
+
65
+ .overflow-content {
66
+ text-align: center;
67
+ color: white;
68
+ }
69
+
70
+ .overflow-count {
71
+ display: block;
72
+ font-size: 24px;
73
+ font-weight: 700;
74
+ margin-bottom: 4px;
75
+ }
76
+
77
+ .overflow-text {
78
+ font-size: 12px;
79
+ opacity: 0.8;
80
+ text-transform: uppercase;
81
+ letter-spacing: 0.5px;
82
+ }
83
+
84
+ /* Responsive grid adjustments */
85
+ @media (max-width: 1024px) {
86
+ .participant-grid {
87
+ gap: 6px;
88
+ padding: 12px 12px 8px 12px;
89
+ }
90
+ }
91
+
92
+ @media (max-width: 768px) {
93
+ .participant-grid {
94
+ gap: 4px;
95
+ padding: 8px 8px 4px 8px;
96
+ }
97
+
98
+ /* Force smaller grids on mobile */
99
+ .participant-grid[style*="repeat(4"] {
100
+ grid-template-columns: repeat(2, 1fr) !important;
101
+ grid-template-rows: repeat(2, 1fr) !important;
102
+ }
103
+
104
+ .participant-grid[style*="repeat(3"] {
105
+ grid-template-columns: repeat(2, 1fr) !important;
106
+ grid-template-rows: repeat(2, 1fr) !important;
107
+ }
108
+ }
109
+
110
+ @media (max-width: 480px) {
111
+ .participant-grid {
112
+ gap: 2px;
113
+ padding: 4px 4px 2px 4px;
114
+ }
115
+
116
+ /* Single column on very small screens */
117
+ .participant-grid[style*="repeat("] {
118
+ grid-template-columns: 1fr !important;
119
+ grid-template-rows: repeat(auto-fit, minmax(200px, 1fr)) !important;
120
+ }
121
+ }
122
+
123
+ /* Animation for grid changes */
124
+ .participant-grid {
125
+ transition: grid-template-columns 0.3s ease, grid-template-rows 0.3s ease;
126
+ }
127
+
128
+ /* Special layout for exactly 2 participants */
129
+ .participant-grid[style*="repeat(2, 1fr)"]:not([style*="rows"]) {
130
+ grid-template-rows: 1fr;
131
+ }
132
+
133
+ /* Special layout for exactly 3 participants */
134
+ .participant-grid[style*="repeat(3, 1fr)"][style*="repeat(2, 1fr)"] .participant-tile:last-child {
135
+ grid-column: 2;
136
+ }
137
+
138
+ /* Ensure tiles maintain aspect ratio in different grid configurations */
139
+ .participant-grid .participant-tile {
140
+ min-height: 120px;
141
+ }
142
+
143
+ @media (min-width: 1200px) {
144
+ .participant-grid .participant-tile {
145
+ min-height: 160px;
146
+ }
147
+ }
148
+
149
+ @media (min-width: 1600px) {
150
+ .participant-grid .participant-tile {
151
+ min-height: 200px;
152
+ }
153
+ }
@@ -0,0 +1,87 @@
1
+ import { useMemo, useState } from 'react';
2
+ import { useParticipants } from 'vg-x07df';
3
+ import { ParticipantTile } from './ParticipantTile';
4
+ import { PaginationControls } from './PaginationControls';
5
+ import './ParticipantGrid.css';
6
+
7
+ interface ParticipantGridProps {
8
+ className?: string;
9
+ }
10
+
11
+ export function ParticipantGrid({ className = '' }: ParticipantGridProps) {
12
+ const [currentPage, setCurrentPage] = useState(1);
13
+ const participantData = useParticipants(undefined, { page: currentPage, pageSize: 8, kind: "active" });
14
+
15
+ const {
16
+ participants,
17
+ totalPages,
18
+ hasNextPage,
19
+ hasPreviousPage,
20
+ totalParticipants,
21
+ } = participantData;
22
+
23
+ const gridLayout = useMemo(() => {
24
+ const count = participants.length;
25
+
26
+ if (count === 0) return { cols: 1, rows: 1 };
27
+ if (count === 1) return { cols: 1, rows: 1 };
28
+ if (count === 2) return { cols: 2, rows: 1 };
29
+ if (count <= 4) return { cols: 2, rows: 2 };
30
+ if (count <= 6) return { cols: 3, rows: 2 };
31
+ if (count <= 8) return { cols: 4, rows: 2 };
32
+
33
+ return { cols: 4, rows: 2 };
34
+ }, [participants.length]);
35
+
36
+ const handlePreviousPage = () => {
37
+ setCurrentPage(prev => Math.max(1, prev - 1));
38
+ };
39
+
40
+ const handleNextPage = () => {
41
+ setCurrentPage(prev => Math.min(totalPages, prev + 1));
42
+ };
43
+
44
+ if (totalParticipants === 0) {
45
+ return (
46
+ <div className={`participant-grid empty ${className}`}>
47
+ <div className="empty-state">
48
+ <div className="empty-icon">👥</div>
49
+ <h3>No participants yet</h3>
50
+ <p>Waiting for others to join the call...</p>
51
+ </div>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ return (
57
+ <div className={`participant-grid-container ${className}`}>
58
+ <div
59
+ className="participant-grid"
60
+ style={{
61
+ gridTemplateColumns: `repeat(${gridLayout.cols}, 1fr)`,
62
+ gridTemplateRows: `repeat(${gridLayout.rows}, 1fr)`,
63
+ }}
64
+ >
65
+ {participants.map((participant: any) => (
66
+ <ParticipantTile
67
+ key={participant.id}
68
+ participant={participant}
69
+ isLocal={participant.id === 'local'}
70
+ videoTrack={null}
71
+ isMuted={false}
72
+ isVideoEnabled={true}
73
+ />
74
+ ))}
75
+ </div>
76
+
77
+ <PaginationControls
78
+ currentPage={currentPage}
79
+ totalPages={totalPages}
80
+ onPreviousPage={handlePreviousPage}
81
+ onNextPage={handleNextPage}
82
+ hasNextPage={hasNextPage}
83
+ hasPreviousPage={hasPreviousPage}
84
+ />
85
+ </div>
86
+ );
87
+ }