@midscene/playground-app 1.7.3 → 1.7.4

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 (50) hide show
  1. package/dist/es/PlaygroundApp.css +0 -128
  2. package/dist/es/PlaygroundApp.mjs +74 -464
  3. package/dist/es/PlaygroundThemeProvider.mjs +10 -0
  4. package/dist/es/PreviewRenderer.mjs +4 -1
  5. package/dist/es/ScrcpyPanel.mjs +97 -89
  6. package/dist/es/SessionSetupPanel.css +292 -0
  7. package/dist/es/SessionSetupPanel.mjs +60 -39
  8. package/dist/es/controller/ai-config.mjs +37 -0
  9. package/dist/es/controller/auto-create.mjs +19 -0
  10. package/dist/es/controller/selectors.mjs +66 -0
  11. package/dist/es/controller/types.mjs +0 -0
  12. package/dist/es/controller/usePlaygroundController.mjs +356 -0
  13. package/dist/es/icons/dropdown-chevron.mjs +61 -0
  14. package/dist/es/icons/midscene-logo.mjs +247 -0
  15. package/dist/es/index.mjs +4 -1
  16. package/dist/es/panels/PlaygroundConversationPanel.css +20 -0
  17. package/dist/es/panels/PlaygroundConversationPanel.mjs +134 -0
  18. package/dist/es/scrcpy-preview.mjs +30 -0
  19. package/dist/lib/PlaygroundApp.css +0 -128
  20. package/dist/lib/PlaygroundApp.js +70 -460
  21. package/dist/lib/PlaygroundThemeProvider.js +44 -0
  22. package/dist/lib/PreviewRenderer.js +4 -1
  23. package/dist/lib/ScrcpyPanel.js +96 -88
  24. package/dist/lib/SessionSetupPanel.css +292 -0
  25. package/dist/lib/SessionSetupPanel.js +70 -38
  26. package/dist/lib/controller/ai-config.js +74 -0
  27. package/dist/lib/controller/auto-create.js +59 -0
  28. package/dist/lib/controller/selectors.js +103 -0
  29. package/dist/lib/controller/types.js +18 -0
  30. package/dist/lib/controller/usePlaygroundController.js +390 -0
  31. package/dist/lib/icons/dropdown-chevron.js +95 -0
  32. package/dist/lib/icons/midscene-logo.js +281 -0
  33. package/dist/lib/index.js +14 -2
  34. package/dist/lib/panels/PlaygroundConversationPanel.css +20 -0
  35. package/dist/lib/panels/PlaygroundConversationPanel.js +168 -0
  36. package/dist/lib/scrcpy-preview.js +79 -0
  37. package/dist/types/PlaygroundPreview.d.ts +6 -0
  38. package/dist/types/PlaygroundThemeProvider.d.ts +2 -0
  39. package/dist/types/PreviewRenderer.d.ts +7 -1
  40. package/dist/types/ScrcpyPanel.d.ts +14 -1
  41. package/dist/types/SessionSetupPanel.d.ts +4 -3
  42. package/dist/types/controller/ai-config.d.ts +4 -0
  43. package/dist/types/controller/auto-create.d.ts +15 -0
  44. package/dist/types/controller/selectors.d.ts +5 -0
  45. package/dist/types/controller/types.d.ts +36 -0
  46. package/dist/types/controller/usePlaygroundController.d.ts +9 -0
  47. package/dist/types/index.d.ts +5 -0
  48. package/dist/types/panels/PlaygroundConversationPanel.d.ts +20 -0
  49. package/dist/types/scrcpy-preview.d.ts +11 -0
  50. package/package.json +4 -3
@@ -1,9 +1,10 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { ScrcpyVideoCodecId } from "@yume-chan/scrcpy";
3
3
  import { BitmapVideoFrameRenderer, WebCodecsVideoDecoder, WebGLVideoFrameRenderer } from "@yume-chan/scrcpy-decoder-webcodecs";
4
- import { Alert, Button, Card, Space, Spin, Typography } from "antd";
4
+ import { Alert, Spin, Typography } from "antd";
5
5
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
6
  import { io } from "socket.io-client";
7
+ import { SCRCPY_METADATA_TIMEOUT_MS, getDefaultScrcpyWaitingStatusText, getScrcpyDecoderStatusText, getScrcpyMetadataTimeoutMessage, getScrcpyPreviewStatusText, isScrcpyPreviewStatusEvent } from "./scrcpy-preview.mjs";
7
8
  import { createScrcpyVideoStream } from "./scrcpy-stream.mjs";
8
9
  function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
9
10
  try {
@@ -32,30 +33,34 @@ function _async_to_generator(fn) {
32
33
  };
33
34
  }
34
35
  const { Text } = Typography;
35
- function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
36
+ function ScrcpyPanel({ connectingOverlay, onStatusChange, renderErrorOverlay, serverUrl, metadataTimeoutMs = SCRCPY_METADATA_TIMEOUT_MS, reconnectInterval = 3000 }) {
36
37
  const canvasStageRef = useRef(null);
37
38
  const socketRef = useRef(null);
38
39
  const decoderRef = useRef(null);
40
+ const metadataTimeoutRef = useRef(null);
39
41
  const reconnectTimerRef = useRef(null);
40
- const connectRef = useRef(null);
41
- const disconnectRef = useRef(null);
42
- const manuallyDisconnectedRef = useRef(false);
42
+ const ignoreDisconnectRef = useRef(false);
43
43
  const [status, setStatus] = useState('connecting');
44
44
  const [errorMessage, setErrorMessage] = useState(null);
45
- const [screenInfo, setScreenInfo] = useState(null);
45
+ const [waitingStatusMessage, setWaitingStatusMessage] = useState(()=>getDefaultScrcpyWaitingStatusText());
46
46
  const [webCodecsSupported, setWebCodecsSupported] = useState(true);
47
- const statusText = useMemo(()=>{
48
- switch(status){
49
- case 'connected':
50
- return 'Live scrcpy preview connected';
51
- case 'error':
52
- return 'Unable to start scrcpy preview';
53
- case 'disconnected':
54
- return 'scrcpy preview disconnected, retrying…';
55
- default:
56
- return 'Connecting to scrcpy preview…';
57
- }
47
+ const [retryNonce, setRetryNonce] = useState(0);
48
+ const statusText = useMemo(()=>getScrcpyPreviewStatusText(status, waitingStatusMessage), [
49
+ status,
50
+ waitingStatusMessage
51
+ ]);
52
+ const showCustomErrorOverlay = ('error' === status || 'disconnected' === status) && Boolean(renderErrorOverlay);
53
+ const renderResolvedErrorOverlay = renderErrorOverlay;
54
+ const requestRetry = useCallback(()=>{
55
+ setStatus('connecting');
56
+ setErrorMessage(null);
57
+ setWaitingStatusMessage(getDefaultScrcpyWaitingStatusText());
58
+ setRetryNonce((current)=>current + 1);
59
+ }, []);
60
+ useEffect(()=>{
61
+ null == onStatusChange || onStatusChange(status);
58
62
  }, [
63
+ onStatusChange,
59
64
  status
60
65
  ]);
61
66
  const clearCanvas = ()=>{
@@ -68,16 +73,12 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
68
73
  decoderRef.current.dispose();
69
74
  decoderRef.current = null;
70
75
  };
71
- const handleConnect = useCallback(()=>{
72
- var _connectRef_current;
73
- manuallyDisconnectedRef.current = false;
74
- null == (_connectRef_current = connectRef.current) || _connectRef_current.call(connectRef);
75
- }, []);
76
- const handleDisconnect = useCallback(()=>{
77
- var _disconnectRef_current;
78
- manuallyDisconnectedRef.current = true;
79
- null == (_disconnectRef_current = disconnectRef.current) || _disconnectRef_current.call(disconnectRef);
80
- }, []);
76
+ const clearMetadataTimeout = ()=>{
77
+ if (metadataTimeoutRef.current) {
78
+ clearTimeout(metadataTimeoutRef.current);
79
+ metadataTimeoutRef.current = null;
80
+ }
81
+ };
81
82
  useEffect(()=>{
82
83
  if (!serverUrl) {
83
84
  setStatus('error');
@@ -92,6 +93,7 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
92
93
  }
93
94
  let disposed = false;
94
95
  const cleanup = ()=>{
96
+ clearMetadataTimeout();
95
97
  if (reconnectTimerRef.current) {
96
98
  clearTimeout(reconnectTimerRef.current);
97
99
  reconnectTimerRef.current = null;
@@ -104,7 +106,7 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
104
106
  clearCanvas();
105
107
  };
106
108
  const scheduleReconnect = ()=>{
107
- if (disposed || reconnectTimerRef.current || manuallyDisconnectedRef.current) return;
109
+ if (disposed || reconnectTimerRef.current) return;
108
110
  reconnectTimerRef.current = setTimeout(()=>{
109
111
  reconnectTimerRef.current = null;
110
112
  cleanup();
@@ -124,20 +126,14 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
124
126
  codec: codecId,
125
127
  renderer
126
128
  });
127
- decoder.sizeChanged(({ width, height })=>{
128
- setScreenInfo({
129
- width,
130
- height
131
- });
132
- });
133
129
  return decoder;
134
130
  })();
135
131
  const connect = ()=>{
136
132
  if (disposed) return;
137
- manuallyDisconnectedRef.current = false;
133
+ ignoreDisconnectRef.current = false;
138
134
  setStatus('connecting');
139
135
  setErrorMessage(null);
140
- setScreenInfo(null);
136
+ setWaitingStatusMessage(getDefaultScrcpyWaitingStatusText());
141
137
  const socket = io(serverUrl, {
142
138
  withCredentials: true,
143
139
  reconnection: false,
@@ -146,113 +142,100 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
146
142
  socketRef.current = socket;
147
143
  const videoStream = createScrcpyVideoStream(socket);
148
144
  socket.on('connect', ()=>{
145
+ setStatus('waiting-for-stream');
146
+ setWaitingStatusMessage(getDefaultScrcpyWaitingStatusText());
147
+ clearMetadataTimeout();
148
+ metadataTimeoutRef.current = setTimeout(()=>{
149
+ if (disposed) return;
150
+ ignoreDisconnectRef.current = true;
151
+ setStatus('error');
152
+ setErrorMessage(getScrcpyMetadataTimeoutMessage(metadataTimeoutMs));
153
+ setWaitingStatusMessage(getDefaultScrcpyWaitingStatusText());
154
+ socket.disconnect();
155
+ socketRef.current = null;
156
+ scheduleReconnect();
157
+ }, metadataTimeoutMs);
149
158
  socket.emit('connect-device', {
150
159
  maxSize: 1024
151
160
  });
152
161
  });
162
+ socket.on('preview-status', (event)=>{
163
+ if (disposed || !isScrcpyPreviewStatusEvent(event)) return;
164
+ setStatus('waiting-for-stream');
165
+ setWaitingStatusMessage(event.message);
166
+ });
153
167
  socket.on('video-metadata', (metadata)=>_async_to_generator(function*() {
154
168
  try {
169
+ clearMetadataTimeout();
155
170
  disposeDecoder();
171
+ setWaitingStatusMessage(getScrcpyDecoderStatusText());
156
172
  const codecId = metadata.codec ? metadata.codec : ScrcpyVideoCodecId.H264;
157
173
  const decoder = yield createDecoder(codecId);
158
174
  decoderRef.current = decoder;
159
- if (metadata.width && metadata.height) setScreenInfo({
160
- width: metadata.width,
161
- height: metadata.height
162
- });
163
175
  videoStream.pipeTo(decoder.writable).catch((error)=>{
164
176
  if (disposed) return;
165
177
  setStatus('error');
166
178
  setErrorMessage(error.message);
167
179
  scheduleReconnect();
168
180
  });
181
+ setWaitingStatusMessage(getDefaultScrcpyWaitingStatusText());
169
182
  setStatus('connected');
170
183
  } catch (error) {
171
184
  if (disposed) return;
172
185
  setStatus('error');
173
186
  setErrorMessage(error instanceof Error ? error.message : 'Failed to start decoder.');
187
+ setWaitingStatusMessage(getDefaultScrcpyWaitingStatusText());
174
188
  scheduleReconnect();
175
189
  }
176
190
  })());
177
191
  socket.on('disconnect', ()=>{
192
+ clearMetadataTimeout();
178
193
  if (disposed) return;
194
+ if (ignoreDisconnectRef.current) {
195
+ ignoreDisconnectRef.current = false;
196
+ return;
197
+ }
179
198
  setStatus('disconnected');
199
+ setErrorMessage(null);
200
+ setWaitingStatusMessage(getDefaultScrcpyWaitingStatusText());
180
201
  scheduleReconnect();
181
202
  });
182
203
  socket.on('connect_error', (error)=>{
204
+ clearMetadataTimeout();
183
205
  if (disposed) return;
184
206
  setStatus('error');
185
207
  setErrorMessage(error.message);
208
+ setWaitingStatusMessage(getDefaultScrcpyWaitingStatusText());
186
209
  scheduleReconnect();
187
210
  });
188
211
  socket.on('error', (error)=>{
212
+ clearMetadataTimeout();
189
213
  if (disposed) return;
190
214
  setStatus('error');
191
215
  setErrorMessage(error.message);
216
+ setWaitingStatusMessage(getDefaultScrcpyWaitingStatusText());
192
217
  scheduleReconnect();
193
218
  });
194
219
  };
195
- const disconnect = ()=>{
196
- cleanup();
197
- setStatus('disconnected');
198
- setErrorMessage(null);
199
- setScreenInfo(null);
200
- };
201
- connectRef.current = connect;
202
- disconnectRef.current = disconnect;
203
220
  connect();
204
221
  return ()=>{
205
222
  disposed = true;
206
- connectRef.current = null;
207
- disconnectRef.current = null;
208
223
  cleanup();
209
224
  };
210
225
  }, [
226
+ metadataTimeoutMs,
211
227
  reconnectInterval,
228
+ retryNonce,
212
229
  serverUrl
213
230
  ]);
214
- return /*#__PURE__*/ jsxs(Card, {
215
- size: "small",
216
- title: "Live scrcpy preview",
231
+ return /*#__PURE__*/ jsxs("div", {
217
232
  style: {
218
233
  height: '100%',
219
234
  display: 'flex',
220
235
  flexDirection: 'column'
221
236
  },
222
- styles: {
223
- body: {
224
- flex: 1,
225
- display: 'flex',
226
- flexDirection: 'column',
227
- minHeight: 0
228
- }
229
- },
230
- extra: /*#__PURE__*/ jsxs(Space, {
231
- size: "small",
232
- children: [
233
- screenInfo ? /*#__PURE__*/ jsxs(Text, {
234
- type: "secondary",
235
- children: [
236
- screenInfo.width,
237
- " \xd7 ",
238
- screenInfo.height
239
- ]
240
- }) : null,
241
- 'connected' === status ? /*#__PURE__*/ jsx(Button, {
242
- size: "small",
243
- onClick: handleDisconnect,
244
- children: "Disconnect"
245
- }) : /*#__PURE__*/ jsx(Button, {
246
- size: "small",
247
- type: "primary",
248
- loading: 'connecting' === status,
249
- onClick: handleConnect,
250
- children: "Connect"
251
- })
252
- ]
253
- }),
254
237
  children: [
255
- errorMessage ? /*#__PURE__*/ jsx(Alert, {
238
+ errorMessage && !showCustomErrorOverlay ? /*#__PURE__*/ jsx(Alert, {
256
239
  type: "warning",
257
240
  showIcon: true,
258
241
  style: {
@@ -284,7 +267,26 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
284
267
  justifyContent: 'center'
285
268
  }
286
269
  }),
287
- 'connected' !== status && /*#__PURE__*/ jsxs("div", {
270
+ 'connected' !== status ? showCustomErrorOverlay && renderResolvedErrorOverlay ? /*#__PURE__*/ jsx("div", {
271
+ style: {
272
+ position: 'absolute',
273
+ inset: 0,
274
+ zIndex: 1
275
+ },
276
+ children: renderResolvedErrorOverlay({
277
+ errorMessage,
278
+ retry: requestRetry,
279
+ status,
280
+ statusText
281
+ })
282
+ }) : 'error' !== status && 'disconnected' !== status && connectingOverlay ? /*#__PURE__*/ jsx("div", {
283
+ style: {
284
+ position: 'absolute',
285
+ inset: 0,
286
+ zIndex: 1
287
+ },
288
+ children: connectingOverlay
289
+ }) : /*#__PURE__*/ jsxs("div", {
288
290
  style: {
289
291
  position: 'absolute',
290
292
  inset: 0,
@@ -300,7 +302,7 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
300
302
  zIndex: 1
301
303
  },
302
304
  children: [
303
- /*#__PURE__*/ jsx(Spin, {
305
+ 'error' === status ? null : /*#__PURE__*/ jsx(Spin, {
304
306
  spinning: true
305
307
  }),
306
308
  /*#__PURE__*/ jsx(Text, {
@@ -309,6 +311,12 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
309
311
  },
310
312
  children: statusText
311
313
  }),
314
+ 'error' === status ? /*#__PURE__*/ jsx(Text, {
315
+ style: {
316
+ color: '#d1d5db'
317
+ },
318
+ children: "Scrcpy preview will retry automatically."
319
+ }) : null,
312
320
  !webCodecsSupported && /*#__PURE__*/ jsx(Text, {
313
321
  style: {
314
322
  color: '#d1d5db'
@@ -316,7 +324,7 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
316
324
  children: "Please use a modern Chromium browser to view the stream."
317
325
  })
318
326
  ]
319
- })
327
+ }) : null
320
328
  ]
321
329
  })
322
330
  ]
@@ -0,0 +1,292 @@
1
+ .session-setup-panel {
2
+ background: #fff;
3
+ flex-direction: column;
4
+ flex: 1;
5
+ justify-content: flex-start;
6
+ align-items: center;
7
+ padding: 0 56px;
8
+ display: flex;
9
+ }
10
+
11
+ .session-setup-card {
12
+ flex-direction: column;
13
+ align-items: center;
14
+ width: 100%;
15
+ max-width: 288px;
16
+ margin-top: 96px;
17
+ display: flex;
18
+ }
19
+
20
+ .session-setup-logo {
21
+ object-fit: contain;
22
+ flex-shrink: 0;
23
+ width: 51px;
24
+ height: 48px;
25
+ }
26
+
27
+ .session-setup-title {
28
+ color: #000;
29
+ text-align: center;
30
+ white-space: pre-line;
31
+ width: 240px;
32
+ margin: 16px 0 0;
33
+ font-family: Roboto, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif;
34
+ font-size: 18px;
35
+ font-weight: 600;
36
+ line-height: 22px;
37
+ }
38
+
39
+ .session-setup-description {
40
+ color: rgba(0, 0, 0, .7);
41
+ text-align: center;
42
+ width: 276px;
43
+ margin: 14.7px 0 0;
44
+ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif;
45
+ font-size: 12px;
46
+ font-weight: 400;
47
+ line-height: 20px;
48
+ }
49
+
50
+ .session-setup-alert.ant-alert {
51
+ width: 100%;
52
+ margin-top: 16px;
53
+ }
54
+
55
+ .session-setup-form {
56
+ width: 100%;
57
+ margin-top: 24px;
58
+ }
59
+
60
+ .session-setup-form .ant-form-item {
61
+ margin-bottom: 16px;
62
+ }
63
+
64
+ .session-setup-form .ant-form-item-label {
65
+ padding-bottom: 4px;
66
+ }
67
+
68
+ .session-setup-form .ant-form-item-label > label {
69
+ color: rgba(0, 0, 0, .5);
70
+ height: 15px;
71
+ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif;
72
+ font-size: 12px;
73
+ font-weight: 500;
74
+ line-height: 14.5px;
75
+ }
76
+
77
+ .session-setup-form .ant-form-item-label > label.ant-form-item-required:before {
78
+ color: rgba(0, 0, 0, .5);
79
+ }
80
+
81
+ .session-setup-form .ant-form-item-label > label.ant-form-item-required:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))):before {
82
+ margin-right: 2px;
83
+ }
84
+
85
+ .session-setup-form .ant-form-item-label > label.ant-form-item-required:not(:-moz-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))):before {
86
+ margin-right: 2px;
87
+ }
88
+
89
+ .session-setup-form .ant-form-item-label > label.ant-form-item-required:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))):before {
90
+ margin-right: 2px;
91
+ }
92
+
93
+ .session-setup-form .ant-form-item-label > label.ant-form-item-required:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)):before {
94
+ margin-left: 2px;
95
+ }
96
+
97
+ .session-setup-form .ant-form-item-label > label.ant-form-item-required:-moz-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)):before {
98
+ margin-left: 2px;
99
+ }
100
+
101
+ .session-setup-form .ant-form-item-label > label.ant-form-item-required:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)):before {
102
+ margin-left: 2px;
103
+ }
104
+
105
+ .session-setup-form .ant-select-single .ant-select-selector {
106
+ height: 36px;
107
+ box-shadow: none;
108
+ background: #f2f4f7;
109
+ border: none;
110
+ border-radius: 8px;
111
+ align-items: center;
112
+ padding: 0 12px;
113
+ display: flex;
114
+ }
115
+
116
+ .session-setup-form .ant-select-single .ant-select-selector .ant-select-selection-item, .session-setup-form .ant-select-single .ant-select-selector .ant-select-selection-placeholder {
117
+ color: #000;
118
+ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif;
119
+ font-size: 14px;
120
+ font-weight: 400;
121
+ line-height: 36px;
122
+ }
123
+
124
+ .session-setup-form .ant-select-single .ant-select-selector .ant-select-selection-placeholder {
125
+ color: rgba(0, 0, 0, .4);
126
+ }
127
+
128
+ .session-setup-form .ant-select-arrow:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
129
+ right: 12px;
130
+ }
131
+
132
+ .session-setup-form .ant-select-arrow:not(:-moz-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
133
+ right: 12px;
134
+ }
135
+
136
+ .session-setup-form .ant-select-arrow:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
137
+ right: 12px;
138
+ }
139
+
140
+ .session-setup-form .ant-select-arrow:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
141
+ left: 12px;
142
+ }
143
+
144
+ .session-setup-form .ant-select-arrow:-moz-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
145
+ left: 12px;
146
+ }
147
+
148
+ .session-setup-form .ant-select-arrow:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
149
+ left: 12px;
150
+ }
151
+
152
+ .session-setup-form .ant-input, .session-setup-form .ant-input-number {
153
+ height: 36px;
154
+ box-shadow: none;
155
+ color: #000;
156
+ background: #f2f4f7;
157
+ border: none;
158
+ border-radius: 8px;
159
+ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif;
160
+ font-size: 14px;
161
+ }
162
+
163
+ .session-setup-form .ant-input-number-input {
164
+ height: 36px;
165
+ }
166
+
167
+ .session-setup-select-icon {
168
+ object-fit: contain;
169
+ pointer-events: none;
170
+ width: 16px;
171
+ height: 16px;
172
+ }
173
+
174
+ .session-setup-submit {
175
+ color: #f2f4f7;
176
+ cursor: pointer;
177
+ background: #1979ff;
178
+ border: 0;
179
+ border-radius: 8px;
180
+ width: 100%;
181
+ height: 32px;
182
+ margin-top: 7px;
183
+ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif;
184
+ font-size: 14px;
185
+ font-weight: 500;
186
+ line-height: 22px;
187
+ transition: opacity .2s;
188
+ }
189
+
190
+ .session-setup-submit:hover:not(:disabled) {
191
+ opacity: .9;
192
+ }
193
+
194
+ .session-setup-submit:disabled {
195
+ cursor: not-allowed;
196
+ opacity: .6;
197
+ }
198
+
199
+ .platform-selector-group {
200
+ grid-template-columns: repeat(2, minmax(0, 1fr));
201
+ gap: 12px;
202
+ width: 100%;
203
+ display: grid;
204
+ }
205
+
206
+ .platform-selector-group .ant-radio-button-wrapper {
207
+ white-space: normal;
208
+ background: #fff;
209
+ border-radius: 14px;
210
+ justify-content: flex-start;
211
+ align-items: flex-start;
212
+ height: auto;
213
+ min-height: 92px;
214
+ padding: 14px 16px;
215
+ transition: border-color .2s, box-shadow .2s, transform .2s;
216
+ display: flex;
217
+ }
218
+
219
+ .platform-selector-group .ant-radio-button-wrapper:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
220
+ border-left-width: 1px;
221
+ }
222
+
223
+ .platform-selector-group .ant-radio-button-wrapper:not(:-moz-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
224
+ border-left-width: 1px;
225
+ }
226
+
227
+ .platform-selector-group .ant-radio-button-wrapper:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
228
+ border-left-width: 1px;
229
+ }
230
+
231
+ .platform-selector-group .ant-radio-button-wrapper:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
232
+ border-right-width: 1px;
233
+ }
234
+
235
+ .platform-selector-group .ant-radio-button-wrapper:-moz-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
236
+ border-right-width: 1px;
237
+ }
238
+
239
+ .platform-selector-group .ant-radio-button-wrapper:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
240
+ border-right-width: 1px;
241
+ }
242
+
243
+ .platform-selector-group .ant-radio-button-wrapper:before {
244
+ display: none;
245
+ }
246
+
247
+ .platform-selector-group .ant-radio-button-wrapper:hover {
248
+ border-color: #1677ff;
249
+ transform: translateY(-1px);
250
+ }
251
+
252
+ .platform-selector-group .ant-radio-button-wrapper-checked {
253
+ border-color: #1677ff;
254
+ box-shadow: 0 10px 24px rgba(22, 119, 255, .12);
255
+ }
256
+
257
+ .platform-selector-card .platform-selector-title {
258
+ color: rgba(0, 0, 0, .88);
259
+ font-size: 15px;
260
+ font-weight: 600;
261
+ line-height: 1.4;
262
+ }
263
+
264
+ .platform-selector-card .platform-selector-description {
265
+ color: rgba(0, 0, 0, .5);
266
+ margin-top: 6px;
267
+ font-size: 12px;
268
+ line-height: 1.5;
269
+ }
270
+
271
+ .session-select-option {
272
+ flex-direction: column;
273
+ gap: 2px;
274
+ line-height: 1.4;
275
+ display: flex;
276
+ }
277
+
278
+ .session-select-option-label {
279
+ color: rgba(0, 0, 0, .88);
280
+ }
281
+
282
+ .session-select-option-description {
283
+ color: rgba(0, 0, 0, .45);
284
+ font-size: 12px;
285
+ }
286
+
287
+ @media (max-width: 640px) {
288
+ .platform-selector-group {
289
+ grid-template-columns: 1fr;
290
+ }
291
+ }
292
+