@100mslive/roomkit-react 0.3.14-alpha.0 → 0.3.14-alpha.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. package/dist/Diagnostics/AudioTest.d.ts +2 -0
  2. package/dist/Diagnostics/ConnectivityTest.d.ts +7 -0
  3. package/dist/Diagnostics/Diagnostics.d.ts +2 -0
  4. package/dist/Diagnostics/VideoTest.d.ts +2 -0
  5. package/dist/Diagnostics/components.d.ts +16 -0
  6. package/dist/Diagnostics/hms.d.ts +9 -0
  7. package/dist/Diagnostics/index.d.ts +1 -0
  8. package/dist/{HLSView-USRUP6VG.js → HLSView-42MLSPUI.js} +2 -2
  9. package/dist/{HLSView-7LHIA6HH.css → HLSView-4UFNO3KZ.css} +3 -3
  10. package/dist/{HLSView-7LHIA6HH.css.map → HLSView-4UFNO3KZ.css.map} +1 -1
  11. package/dist/Prebuilt/App.d.ts +3 -1
  12. package/dist/Stats/index.d.ts +1 -0
  13. package/dist/{chunk-DYDYPNYY.js → chunk-VNHKDQOM.js} +12334 -1483
  14. package/dist/chunk-VNHKDQOM.js.map +7 -0
  15. package/dist/index.cjs.css +2 -2
  16. package/dist/index.cjs.css.map +1 -1
  17. package/dist/index.cjs.js +16767 -5828
  18. package/dist/index.cjs.js.map +4 -4
  19. package/dist/index.css +2 -2
  20. package/dist/index.css.map +1 -1
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +5 -1
  23. package/dist/meta.cjs.json +896 -190
  24. package/dist/meta.esbuild.json +908 -198
  25. package/package.json +7 -7
  26. package/src/Diagnostics/AudioTest.tsx +153 -0
  27. package/src/Diagnostics/ConnectivityTest.tsx +355 -0
  28. package/src/Diagnostics/DeviceSelector.jsx +71 -0
  29. package/src/Diagnostics/Diagnostics.tsx +112 -0
  30. package/src/Diagnostics/VideoTest.tsx +60 -0
  31. package/src/Diagnostics/components.tsx +84 -0
  32. package/src/Diagnostics/hms.ts +9 -0
  33. package/src/Diagnostics/index.ts +1 -0
  34. package/src/Prebuilt/App.tsx +12 -1
  35. package/src/Prebuilt/components/Chat/ChatFooter.tsx +16 -2
  36. package/src/Prebuilt/components/StatsForNerds.jsx +1 -13
  37. package/src/Stats/index.tsx +1 -0
  38. package/src/index.ts +1 -0
  39. package/dist/chunk-DYDYPNYY.js.map +0 -7
  40. /package/dist/{HLSView-USRUP6VG.js.map → HLSView-42MLSPUI.js.map} +0 -0
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "prebuilt",
11
11
  "roomkit"
12
12
  ],
13
- "version": "0.3.14-alpha.0",
13
+ "version": "0.3.14-alpha.2",
14
14
  "author": "100ms",
15
15
  "license": "MIT",
16
16
  "repository": {
@@ -74,12 +74,12 @@
74
74
  "react": ">=17.0.2 <19.0.0"
75
75
  },
76
76
  "dependencies": {
77
- "@100mslive/hls-player": "0.3.14-alpha.0",
77
+ "@100mslive/hls-player": "0.3.14-alpha.2",
78
78
  "@100mslive/hms-noise-cancellation": "0.0.1",
79
- "@100mslive/hms-virtual-background": "1.13.14-alpha.0",
80
- "@100mslive/hms-whiteboard": "0.0.4-alpha.0",
81
- "@100mslive/react-icons": "0.10.14-alpha.0",
82
- "@100mslive/react-sdk": "0.10.14-alpha.0",
79
+ "@100mslive/hms-virtual-background": "1.13.14-alpha.2",
80
+ "@100mslive/hms-whiteboard": "0.0.4-alpha.2",
81
+ "@100mslive/react-icons": "0.10.14-alpha.2",
82
+ "@100mslive/react-sdk": "0.10.14-alpha.2",
83
83
  "@100mslive/types-prebuilt": "0.12.9",
84
84
  "@emoji-mart/data": "^1.0.6",
85
85
  "@emoji-mart/react": "^1.0.1",
@@ -115,5 +115,5 @@
115
115
  "uuid": "^8.3.2",
116
116
  "worker-timers": "^7.0.40"
117
117
  },
118
- "gitHead": "8e40f00818c7057c6cdf817982d7ded8a4d44426"
118
+ "gitHead": "3ef383402ec24ac3a20df2a932179c29cd1c834b"
119
119
  }
@@ -0,0 +1,153 @@
1
+ /* eslint-disable react/prop-types */
2
+ import React, { useEffect, useState } from 'react';
3
+ import {
4
+ selectDevices,
5
+ selectLocalAudioTrackID,
6
+ selectLocalMediaSettings,
7
+ selectTrackAudioByID,
8
+ useHMSActions,
9
+ useHMSStore,
10
+ } from '@100mslive/react-sdk';
11
+ import { MicOnIcon, SpeakerIcon } from '@100mslive/react-icons';
12
+ import { TestContainer, TestFooter } from './components';
13
+ import { Button } from '../Button';
14
+ import { Box, Flex } from '../Layout';
15
+ import { Progress } from '../Progress';
16
+ import { Text } from '../Text';
17
+ // @ts-ignore: No implicit any
18
+ import { DeviceSelector } from './DeviceSelector';
19
+ import { hmsDiagnostics } from './hms';
20
+ import { useAudioOutputTest } from '../Prebuilt/components/hooks/useAudioOutputTest';
21
+ import { TEST_AUDIO_URL } from '../Prebuilt/common/constants';
22
+
23
+ const SelectContainer = ({ children }: { children: React.ReactNode }) => (
24
+ <Box css={{ w: '50%', '@lg': { w: '100%' } }}>{children}</Box>
25
+ );
26
+
27
+ const MicTest = () => {
28
+ const devices = useHMSStore(selectDevices);
29
+ const [isRecording, setIsRecording] = useState(false);
30
+ const { audioInputDeviceId } = useHMSStore(selectLocalMediaSettings);
31
+ const [selectedMic, setSelectedMic] = useState(audioInputDeviceId || 'default');
32
+ const trackID = useHMSStore(selectLocalAudioTrackID);
33
+ const audioLevel = useHMSStore(selectTrackAudioByID(trackID));
34
+
35
+ return (
36
+ <SelectContainer>
37
+ <DeviceSelector
38
+ title="Microphone(Input)"
39
+ devices={devices.audioInput}
40
+ selection={selectedMic}
41
+ icon={<MicOnIcon />}
42
+ onChange={(deviceId: string) => {
43
+ setSelectedMic(deviceId);
44
+ hmsDiagnostics.stopMicCheck();
45
+ setIsRecording(false);
46
+ }}
47
+ />
48
+ <Flex css={{ gap: '$6', alignItems: 'center' }}>
49
+ <Button
50
+ onClick={() =>
51
+ hmsDiagnostics
52
+ .startMicCheck(selectedMic, () => {
53
+ setIsRecording(false);
54
+ })
55
+ .then(() => {
56
+ setIsRecording(true);
57
+ })
58
+ }
59
+ disabled={isRecording}
60
+ >
61
+ {isRecording ? 'Recording...' : 'Record'}
62
+ </Button>
63
+ {isRecording && (
64
+ <>
65
+ <Text>
66
+ <MicOnIcon />
67
+ </Text>
68
+ <Progress.Root value={audioLevel} css={{ h: '$2' }}>
69
+ <Progress.Content
70
+ style={{
71
+ transform: `translateX(-${100 - audioLevel}%)`,
72
+ transition: 'transform 0.3s',
73
+ }}
74
+ />
75
+ </Progress.Root>
76
+ </>
77
+ )}
78
+ </Flex>
79
+ </SelectContainer>
80
+ );
81
+ };
82
+
83
+ const SpeakerTest = () => {
84
+ const actions = useHMSActions();
85
+ const devices = useHMSStore(selectDevices);
86
+ const { audioOutputDeviceId } = useHMSStore(selectLocalMediaSettings);
87
+ const { playing, setPlaying, audioRef } = useAudioOutputTest({ deviceId: audioOutputDeviceId || 'default' });
88
+
89
+ return (
90
+ <SelectContainer>
91
+ <DeviceSelector
92
+ title="Speaker(output)"
93
+ devices={devices.audioOutput}
94
+ selection={audioOutputDeviceId || 'default'}
95
+ icon={<SpeakerIcon />}
96
+ onChange={(deviceId: string) => {
97
+ actions.setAudioOutputDevice(deviceId);
98
+ }}
99
+ />
100
+ <Button
101
+ onClick={() => {
102
+ if (audioRef.current) {
103
+ audioRef.current.src = hmsDiagnostics.getRecordedAudio() || TEST_AUDIO_URL;
104
+ audioRef.current.play();
105
+ }
106
+ }}
107
+ disabled={playing}
108
+ >
109
+ <SpeakerIcon />
110
+ <Text css={{ ml: '$4' }}>{playing ? 'Playing' : 'Playback'}</Text>
111
+ </Button>
112
+ <audio
113
+ ref={audioRef}
114
+ onEnded={() => setPlaying(false)}
115
+ onPlay={() => setPlaying(true)}
116
+ style={{ display: 'none' }}
117
+ />
118
+ </SelectContainer>
119
+ );
120
+ };
121
+
122
+ export const AudioTest = () => {
123
+ const [error, setError] = useState<Error | undefined>();
124
+ useEffect(() => {
125
+ hmsDiagnostics.requestPermission({ audio: true }).catch(error => setError(error));
126
+ }, []);
127
+
128
+ return (
129
+ <>
130
+ <TestContainer>
131
+ <Text variant="body2" css={{ c: '$on_primary_medium' }}>
132
+ Record an audio clip and play it back to check that your microphone and speaker are working. If they aren't,
133
+ make sure your volume is turned up, try a different speaker or microphone, or check your bluetooth settings.
134
+ </Text>
135
+
136
+ <Flex
137
+ css={{
138
+ mt: '$10',
139
+ gap: '$10',
140
+ '@lg': {
141
+ flexDirection: 'column',
142
+ gap: '$8',
143
+ },
144
+ }}
145
+ >
146
+ {!error && <MicTest />}
147
+ <SpeakerTest />
148
+ </Flex>
149
+ </TestContainer>
150
+ <TestFooter error={error} ctaText="Does your audio sound good?" />
151
+ </>
152
+ );
153
+ };
@@ -0,0 +1,355 @@
1
+ import React, { useState } from 'react';
2
+ import { ConnectivityCheckResult, ConnectivityState, DiagnosticsRTCStats } from '@100mslive/react-sdk';
3
+ import { CheckCircleIcon, CrossCircleIcon, LinkIcon } from '@100mslive/react-icons';
4
+ import { TestContainer, TestFooter } from './components';
5
+ import { Button } from '../Button';
6
+ import { Box, Flex } from '../Layout';
7
+ import { Loading } from '../Loading';
8
+ import { formatBytes } from '../Stats';
9
+ import { Text } from '../Text';
10
+ import { hmsDiagnostics } from './hms';
11
+
12
+ const Regions = {
13
+ in: 'India',
14
+ eu: 'Europe',
15
+ us: 'United States',
16
+ };
17
+
18
+ const ConnectivityStateMessage = {
19
+ [ConnectivityState.STARTING]: 'Fetching Init',
20
+ [ConnectivityState.INIT_FETCHED]: 'Connecting to signal server',
21
+ [ConnectivityState.SIGNAL_CONNECTED]: 'Establishing ICE connection',
22
+ [ConnectivityState.ICE_ESTABLISHED]: 'Capturing Media',
23
+ [ConnectivityState.MEDIA_CAPTURED]: 'Publishing Media',
24
+ [ConnectivityState.MEDIA_PUBLISHED]: 'Finishing Up',
25
+ [ConnectivityState.COMPLETED]: 'Completed',
26
+ };
27
+
28
+ export const ConnectivityTestStepResult = ({
29
+ title,
30
+ success,
31
+ children,
32
+ }: {
33
+ title: string;
34
+ success?: boolean;
35
+ children: React.ReactNode;
36
+ }) => {
37
+ return (
38
+ <Box css={{ my: '$10', p: '$10', r: '$1', bg: '$surface_bright' }}>
39
+ <Text css={{ c: '$on_primary_medium', mb: '$6' }}>{title}</Text>
40
+ {success ? (
41
+ <Flex>
42
+ <Text css={{ c: '$alert_success' }}>
43
+ <CheckCircleIcon width="1.5rem" height="1.5rem" />
44
+ </Text>
45
+ <Text variant="lg" css={{ ml: '$4' }}>
46
+ Connected
47
+ </Text>
48
+ </Flex>
49
+ ) : (
50
+ <Flex>
51
+ <Text css={{ c: '$alert_error_bright' }}>
52
+ <CrossCircleIcon width="1.5rem" height="1.5rem" />
53
+ </Text>
54
+ <Text variant="lg" css={{ ml: '$4' }}>
55
+ Failed
56
+ </Text>
57
+ </Flex>
58
+ )}
59
+ <Box>{children}</Box>
60
+ </Box>
61
+ );
62
+ };
63
+
64
+ const DetailedInfo = ({
65
+ title,
66
+ value,
67
+ Icon,
68
+ }: {
69
+ title: string;
70
+ value: string;
71
+ Icon?: (props: React.SVGProps<SVGSVGElement>) => React.JSX.Element;
72
+ }) => {
73
+ return (
74
+ <Box css={{ flex: '50%', mt: '$6' }}>
75
+ <Text variant="caption" css={{ fontWeight: '$semiBold', c: '$on_primary_medium' }}>
76
+ {title}
77
+ </Text>
78
+ <Flex css={{ mt: '$xs', alignItems: 'flex-start' }}>
79
+ {Icon && (
80
+ <Text css={{ mr: '$4' }}>
81
+ <Icon width="1rem" height="1rem" />
82
+ </Text>
83
+ )}
84
+ <Text variant="caption">{value}</Text>
85
+ </Flex>
86
+ </Box>
87
+ );
88
+ };
89
+
90
+ const MediaServerResult = ({ result }: { result?: ConnectivityCheckResult['mediaServerReport'] }) => {
91
+ return (
92
+ <ConnectivityTestStepResult
93
+ title="Media server connection test"
94
+ success={result?.isPublishICEConnected && result.isSubscribeICEConnected}
95
+ >
96
+ <Flex css={{ flexWrap: 'wrap' }}>
97
+ <DetailedInfo
98
+ title="Media Captured"
99
+ value={result?.stats?.audio.bytesSent ? 'Yes' : 'No'}
100
+ Icon={result?.stats?.audio.bytesSent ? CheckCircleIcon : CrossCircleIcon}
101
+ />
102
+ <DetailedInfo
103
+ title="Media Published"
104
+ value={result?.stats?.audio.bitrateSent ? 'Yes' : 'No'}
105
+ Icon={result?.stats?.audio.bytesSent ? CheckCircleIcon : CrossCircleIcon}
106
+ />
107
+ {result?.connectionQualityScore ? (
108
+ <DetailedInfo
109
+ title="Connection Quality Score (CQS)"
110
+ value={`${result.connectionQualityScore.toFixed(2)} (out of 5)`}
111
+ />
112
+ ) : null}
113
+ </Flex>
114
+ </ConnectivityTestStepResult>
115
+ );
116
+ };
117
+
118
+ const SignallingResult = ({ result }: { result?: ConnectivityCheckResult['signallingReport'] }) => {
119
+ return (
120
+ <ConnectivityTestStepResult title="Signalling server connection test" success={result?.isConnected}>
121
+ <Flex css={{ flexWrap: 'wrap' }}>
122
+ <DetailedInfo
123
+ title="Signalling Gateway"
124
+ value={result?.isConnected ? 'Reachable' : 'Unreachable'}
125
+ Icon={result?.isConnected ? CheckCircleIcon : CrossCircleIcon}
126
+ />
127
+ <DetailedInfo title="Websocket URL" value={result?.websocketUrl || 'N/A'} Icon={LinkIcon} />
128
+ </Flex>
129
+ </ConnectivityTestStepResult>
130
+ );
131
+ };
132
+
133
+ const AudioStats = ({ stats }: { stats: DiagnosticsRTCStats | undefined }) => {
134
+ return (
135
+ <ConnectivityTestStepResult title="Audio" success={!!stats?.bytesSent}>
136
+ {stats && (
137
+ <Flex css={{ flexWrap: 'wrap' }}>
138
+ <DetailedInfo title="Bytes Sent" value={formatBytes(stats.bytesSent)} />
139
+ <DetailedInfo title="Bytes Received" value={formatBytes(stats.bytesReceived)} />
140
+ <DetailedInfo title="Packets Received" value={stats.packetsReceived.toString()} />
141
+ <DetailedInfo title="Packets Lost" value={stats.packetsLost.toString()} />
142
+ <DetailedInfo title="Bitrate Sent" value={formatBytes(stats.bitrateSent, 'b/s')} />
143
+ <DetailedInfo title="Bitrate Received" value={formatBytes(stats.bitrateReceived, 'b/s')} />
144
+ <DetailedInfo title="Round Trip Time" value={`${stats.roundTripTime} ms`} />
145
+ </Flex>
146
+ )}
147
+ </ConnectivityTestStepResult>
148
+ );
149
+ };
150
+
151
+ const VideoStats = ({ stats }: { stats: DiagnosticsRTCStats | undefined }) => {
152
+ return (
153
+ <ConnectivityTestStepResult title="Video" success={!!stats?.bytesSent}>
154
+ {stats && (
155
+ <Flex css={{ flexWrap: 'wrap' }}>
156
+ <DetailedInfo title="Bytes Sent" value={formatBytes(stats.bytesSent)} />
157
+ <DetailedInfo title="Bytes Received" value={formatBytes(stats.bytesReceived)} />
158
+ <DetailedInfo title="Packets Received" value={stats.packetsReceived.toString()} />
159
+ <DetailedInfo title="Packets Lost" value={stats.packetsLost.toString()} />
160
+ <DetailedInfo title="Bitrate Sent" value={formatBytes(stats.bitrateSent, 'b/s')} />
161
+ <DetailedInfo title="Bitrate Received" value={formatBytes(stats.bitrateReceived, 'b/s')} />
162
+ <DetailedInfo title="Round Trip Time" value={`${stats.roundTripTime} ms`} />
163
+ </Flex>
164
+ )}
165
+ </ConnectivityTestStepResult>
166
+ );
167
+ };
168
+
169
+ const Footer = ({
170
+ error,
171
+ result,
172
+ restart,
173
+ }: {
174
+ result?: ConnectivityCheckResult;
175
+ restart: () => void;
176
+ error?: Error;
177
+ }) => {
178
+ return (
179
+ <TestFooter error={error}>
180
+ <Flex css={{ gap: '$8', '@lg': { flexDirection: 'column' } }}>
181
+ <Button variant="standard" onClick={restart}>
182
+ Restart Test
183
+ </Button>
184
+ <Button disabled={!result} onClick={() => result && downloadJson(result, 'hms_diagnostics_results')}>
185
+ Download Test Report
186
+ </Button>
187
+ </Flex>
188
+ </TestFooter>
189
+ );
190
+ };
191
+
192
+ const ConnectivityTestReport = ({
193
+ error,
194
+ result,
195
+ progress,
196
+ startTest,
197
+ }: {
198
+ error?: Error;
199
+ result?: ConnectivityCheckResult;
200
+ progress?: ConnectivityState;
201
+ startTest: () => void;
202
+ }) => {
203
+ if (error) {
204
+ return (
205
+ <>
206
+ <TestContainer css={{ textAlign: 'center' }}>
207
+ <Text css={{ c: '$alert_error_default', mb: '$4' }}>
208
+ <CrossCircleIcon />
209
+ </Text>
210
+ <Text variant="h6">Connectivity Test Failed</Text>
211
+ <Text variant="body2" css={{ c: '$on_primary_medium' }}>
212
+ {error.message}
213
+ </Text>
214
+ </TestContainer>
215
+ <Footer restart={startTest} error={error} />
216
+ </>
217
+ );
218
+ }
219
+
220
+ if (result) {
221
+ return (
222
+ <>
223
+ <TestContainer>
224
+ <Text css={{ c: '$on_primary_medium' }}>Connectivity test has been completed.</Text>
225
+ <SignallingResult result={result?.signallingReport} />
226
+ <MediaServerResult result={result?.mediaServerReport} />
227
+ <AudioStats stats={result?.mediaServerReport?.stats?.audio} />
228
+ <VideoStats stats={result?.mediaServerReport?.stats?.video} />
229
+ </TestContainer>
230
+ <Footer result={result} restart={startTest} error={error} />
231
+ </>
232
+ );
233
+ }
234
+
235
+ if (progress) {
236
+ return (
237
+ <TestContainer css={{ textAlign: 'center' }}>
238
+ <Text css={{ c: '$primary_bright' }}>
239
+ <Loading size="3.5rem" color="currentColor" />
240
+ </Text>
241
+ <Text variant="h6" css={{ mt: '$8' }}>
242
+ Checking your connection...
243
+ </Text>
244
+ <Text
245
+ variant="body2"
246
+ css={{ c: '$on_primary_medium', mt: '$4' }}
247
+ >{`${ConnectivityStateMessage[progress]}...`}</Text>
248
+ </TestContainer>
249
+ );
250
+ }
251
+
252
+ return null;
253
+ };
254
+
255
+ const RegionSelector = ({
256
+ region,
257
+ setRegion,
258
+ startTest,
259
+ }: {
260
+ region?: string;
261
+ startTest?: () => void;
262
+ setRegion: (region: string) => void;
263
+ }) => {
264
+ return (
265
+ <TestContainer css={{ borderBottom: '1px solid $border_default' }}>
266
+ <Text variant="body1">Select a region</Text>
267
+ <Text variant="body2" css={{ c: '$on_secondary_low' }}>
268
+ Select the closest region for best results
269
+ </Text>
270
+ <Flex
271
+ justify="between"
272
+ css={{
273
+ mt: '$md',
274
+ '@lg': {
275
+ flexDirection: 'column',
276
+ gap: '$8',
277
+ },
278
+ }}
279
+ >
280
+ <Flex
281
+ css={{
282
+ gap: '$4',
283
+ '@lg': {
284
+ flexDirection: 'column',
285
+ },
286
+ }}
287
+ >
288
+ {Object.entries(Regions).map(([key, value]) => (
289
+ <Button
290
+ key={key}
291
+ outlined={region !== key}
292
+ variant={region === key ? 'primary' : 'standard'}
293
+ css={region === key ? { bg: '$primary_dim' } : {}}
294
+ onClick={() => setRegion(key)}
295
+ >
296
+ {value}
297
+ </Button>
298
+ ))}
299
+ </Flex>
300
+ <Flex css={{ '@lg': { flexDirection: 'column' } }}>
301
+ <Button variant="primary" onClick={startTest} disabled={!startTest}>
302
+ {startTest ? 'Start Test' : 'Testing...'}
303
+ </Button>
304
+ </Flex>
305
+ </Flex>
306
+ </TestContainer>
307
+ );
308
+ };
309
+
310
+ export const ConnectivityTest = () => {
311
+ const [region, setRegion] = useState<string | undefined>(Object.keys(Regions)[0]);
312
+ const [error, setError] = useState<Error | undefined>();
313
+ const [progress, setProgress] = useState<ConnectivityState>();
314
+ const [result, setResult] = useState<ConnectivityCheckResult | undefined>();
315
+
316
+ const startTest = () => {
317
+ setError(undefined);
318
+ setProgress(ConnectivityState.STARTING);
319
+ setResult(undefined);
320
+ hmsDiagnostics
321
+ .startConnectivityCheck(
322
+ state => {
323
+ setProgress(state);
324
+ },
325
+ result => {
326
+ setResult(result);
327
+ },
328
+ region,
329
+ )
330
+ .catch(error => {
331
+ setError(error);
332
+ });
333
+ };
334
+
335
+ return (
336
+ <>
337
+ <RegionSelector
338
+ region={region}
339
+ setRegion={setRegion}
340
+ startTest={!progress || progress === ConnectivityState.COMPLETED ? startTest : undefined}
341
+ />
342
+ <ConnectivityTestReport error={error} result={result} progress={progress} startTest={startTest} />
343
+ </>
344
+ );
345
+ };
346
+
347
+ const downloadJson = (obj: object, fileName: string) => {
348
+ const a = document.createElement('a');
349
+ const file = new Blob([JSON.stringify(obj, null, 2)], {
350
+ type: 'application/json',
351
+ });
352
+ a.href = URL.createObjectURL(file);
353
+ a.download = `${fileName}.json`;
354
+ a.click();
355
+ };
@@ -0,0 +1,71 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Dropdown } from '../Dropdown';
4
+ import { Box, Flex } from '../Layout';
5
+ import { DialogDropdownTrigger } from '../Prebuilt/primitives/DropdownTrigger';
6
+ import { Text } from '../Text';
7
+
8
+ export const DeviceSelector = ({ title, devices, selection, onChange, icon, children = null }) => {
9
+ const [open, setOpen] = useState(false);
10
+ const ref = useRef(null);
11
+ return (
12
+ <Box css={{ mb: '$6' }}>
13
+ <Text css={{ mb: '$4' }}>{title}</Text>
14
+ <Flex
15
+ align="center"
16
+ css={{
17
+ gap: '$4',
18
+ '@md': {
19
+ flexDirection: children ? 'column' : 'row',
20
+ alignItems: children ? 'start' : 'center',
21
+ },
22
+ }}
23
+ >
24
+ <Dropdown.Root open={open} onOpenChange={setOpen}>
25
+ <DialogDropdownTrigger
26
+ ref={ref}
27
+ icon={icon}
28
+ title={devices.find(({ deviceId }) => deviceId === selection)?.label || 'Select device from list'}
29
+ open={open}
30
+ />
31
+ <Dropdown.Portal>
32
+ <Dropdown.Content
33
+ align="start"
34
+ sideOffset={8}
35
+ css={{
36
+ w:
37
+ // @ts-ignore
38
+ ref.current?.clientWidth,
39
+ zIndex: 1001,
40
+ }}
41
+ >
42
+ {devices.map(device => {
43
+ return (
44
+ <Dropdown.Item
45
+ key={device.label}
46
+ onSelect={() => onChange(device.deviceId)}
47
+ css={{
48
+ px: '$9',
49
+ }}
50
+ >
51
+ {device.label}
52
+ </Dropdown.Item>
53
+ );
54
+ })}
55
+ </Dropdown.Content>
56
+ </Dropdown.Portal>
57
+ </Dropdown.Root>
58
+ {children}
59
+ </Flex>
60
+ </Box>
61
+ );
62
+ };
63
+
64
+ DeviceSelector.propTypes = {
65
+ title: PropTypes.string.isRequired,
66
+ devices: PropTypes.array.isRequired,
67
+ selection: PropTypes.string,
68
+ onChange: PropTypes.func.isRequired,
69
+ icon: PropTypes.node,
70
+ children: PropTypes.node,
71
+ };