@vanira/sdk-react-native 0.0.2

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 (148) hide show
  1. package/README.md +239 -0
  2. package/package.json +53 -0
  3. package/src/__tests__/WebRTCClient.integration.test.ts +396 -0
  4. package/src/__tests__/adapters.test.ts +475 -0
  5. package/src/__tests__/httpResponse.test.ts +25 -0
  6. package/src/__tests__/mocks/react-native-incall-manager.ts +8 -0
  7. package/src/__tests__/mocks/react-native-permissions.ts +15 -0
  8. package/src/__tests__/mocks/react-native-webrtc.ts +6 -0
  9. package/src/__tests__/mocks/react-native.ts +28 -0
  10. package/src/__tests__/preset.test.ts +239 -0
  11. package/src/__tests__/resolveRuntimeConfig.test.ts +90 -0
  12. package/src/__tests__/storage.test.ts +211 -0
  13. package/src/__tests__/webrtcSignaling.test.ts +42 -0
  14. package/src/adapters/PeerConnectionAdapter.ts +101 -0
  15. package/src/adapters/browser/BrowserAudioAdapter.ts +43 -0
  16. package/src/adapters/browser/BrowserDataChannelAdapter.ts +69 -0
  17. package/src/adapters/browser/BrowserMediaAdapter.ts +15 -0
  18. package/src/adapters/browser/BrowserPeerAdapter.ts +14 -0
  19. package/src/adapters/browser/index.ts +4 -0
  20. package/src/adapters/interfaces.ts +84 -0
  21. package/src/adapters/react-native/RNAudioAdapter.ts +42 -0
  22. package/src/adapters/react-native/RNDataChannelAdapter.ts +79 -0
  23. package/src/adapters/react-native/RNMediaAdapter.ts +46 -0
  24. package/src/adapters/react-native/RNPeerAdapter.ts +28 -0
  25. package/src/adapters/react-native/callAudioRouting.ts +115 -0
  26. package/src/adapters/react-native/decodeUtf8.ts +72 -0
  27. package/src/adapters/react-native/index.ts +4 -0
  28. package/src/adapters/react-native/rnUploadFile.ts +76 -0
  29. package/src/adapters/storage/BrowserDualStorageAdapter.ts +71 -0
  30. package/src/adapters/storage/MemoryStorageAdapter.ts +50 -0
  31. package/src/adapters/storage/StorageAdapter.ts +21 -0
  32. package/src/adapters/storage/createSyncStorageAdapter.ts +40 -0
  33. package/src/adapters/storage/index.ts +7 -0
  34. package/src/api/services/ChatService.ts +304 -0
  35. package/src/api/services/ConfigService.ts +33 -0
  36. package/src/assets/icons.js +35 -0
  37. package/src/cdn.ts +68 -0
  38. package/src/core/CallSessionStore.ts +137 -0
  39. package/src/core/DraggableController.ts +83 -0
  40. package/src/core/SessionManager.ts +322 -0
  41. package/src/core/VaniraAI.ts +464 -0
  42. package/src/core/WebRTCClient.ts +1012 -0
  43. package/src/core/httpResponse.ts +22 -0
  44. package/src/core/iceServers.ts +18 -0
  45. package/src/core/toolCallNormalize.ts +80 -0
  46. package/src/core/voice-client.js +236 -0
  47. package/src/core/webrtcSignaling.ts +72 -0
  48. package/src/index.js +34 -0
  49. package/src/index.ts +6 -0
  50. package/src/platforms/browser.ts +67 -0
  51. package/src/platforms/react-native.ts +105 -0
  52. package/src/presets/BookingCalendarModal.tsx +457 -0
  53. package/src/presets/CameraModal.tsx +576 -0
  54. package/src/presets/DynamicFormModal.tsx +378 -0
  55. package/src/presets/NativePresetRenderer.tsx +350 -0
  56. package/src/presets/NavigateHandler.tsx +75 -0
  57. package/src/presets/PresetHost.tsx +155 -0
  58. package/src/presets/PresetShellModal.tsx +97 -0
  59. package/src/presets/UploadModal.tsx +321 -0
  60. package/src/presets/calendar/calendarUtils.ts +386 -0
  61. package/src/presets/call/CallSpeakerToggle.tsx +59 -0
  62. package/src/presets/call/callAudioRouting.ts +2 -0
  63. package/src/presets/call/useCallSpeaker.ts +31 -0
  64. package/src/presets/camera/cameraPermissions.ts +18 -0
  65. package/src/presets/camera/cameraStream.ts +19 -0
  66. package/src/presets/camera/cameraUtils.ts +21 -0
  67. package/src/presets/camera/useLivenessFlow.ts +95 -0
  68. package/src/presets/chalkboard/ChalkboardOverlay.tsx +156 -0
  69. package/src/presets/chalkboard/EraseTextHandler.tsx +95 -0
  70. package/src/presets/chalkboard/TypeTextHandler.tsx +107 -0
  71. package/src/presets/chalkboard/boardAbort.ts +36 -0
  72. package/src/presets/chalkboard/boardQueue.ts +620 -0
  73. package/src/presets/chalkboard/chalkboardSession.ts +75 -0
  74. package/src/presets/chalkboard/drawUtils.ts +123 -0
  75. package/src/presets/chalkboard/textUtils.ts +109 -0
  76. package/src/presets/clipRegion/ClipRegionModal.tsx +261 -0
  77. package/src/presets/clipRegion/clipRegionBridge.ts +19 -0
  78. package/src/presets/form/formValidation.ts +104 -0
  79. package/src/presets/form/parseFormFields.ts +171 -0
  80. package/src/presets/host/HostElementPresetHandler.tsx +155 -0
  81. package/src/presets/host/hostPresetBridge.ts +71 -0
  82. package/src/presets/index.ts +63 -0
  83. package/src/presets/liveScreen/CloseLiveScreenHandler.tsx +36 -0
  84. package/src/presets/liveScreen/LiveScreenCaptureHost.tsx +312 -0
  85. package/src/presets/liveScreen/LiveScreenHandler.tsx +25 -0
  86. package/src/presets/liveScreen/LiveScreenPipOverlay.tsx +6 -0
  87. package/src/presets/liveScreen/liveScreenSession.ts +73 -0
  88. package/src/presets/liveVision/CloseLiveVisionHandler.tsx +29 -0
  89. package/src/presets/liveVision/LiveVisionCameraHost.tsx +317 -0
  90. package/src/presets/liveVision/LiveVisionHandler.tsx +26 -0
  91. package/src/presets/liveVision/LiveVisionPipOverlay.tsx +7 -0
  92. package/src/presets/liveVision/liveVisionFrameLoop.ts +38 -0
  93. package/src/presets/liveVision/liveVisionSession.ts +75 -0
  94. package/src/presets/liveVision/liveVisionUpload.ts +62 -0
  95. package/src/presets/navigation/internalRouteRegistry.ts +25 -0
  96. package/src/presets/navigation/navigationBridge.ts +76 -0
  97. package/src/presets/navigation/navigationTypes.ts +12 -0
  98. package/src/presets/parseToolCall.ts +60 -0
  99. package/src/presets/presetClientAdapter.ts +29 -0
  100. package/src/presets/presetCompletion.ts +91 -0
  101. package/src/presets/presetEventHelpers.ts +45 -0
  102. package/src/presets/registry.ts +128 -0
  103. package/src/presets/streaming/mediaFrameUpload.ts +93 -0
  104. package/src/presets/types.ts +74 -0
  105. package/src/presets/upload/pickUploadFile.ts +256 -0
  106. package/src/presets/upload/uploadFormats.ts +163 -0
  107. package/src/presets/upload/uploadUtils.ts +68 -0
  108. package/src/react/PresetRenderer.tsx +144 -0
  109. package/src/react/index.ts +1 -0
  110. package/src/runtime/browserRuntime.ts +54 -0
  111. package/src/runtime/platform.ts +17 -0
  112. package/src/runtime/reactNativeRuntime.ts +68 -0
  113. package/src/runtime/resolveRuntimeConfig.ts +75 -0
  114. package/src/runtime/runtimeBundles.ts +74 -0
  115. package/src/runtime/types.ts +135 -0
  116. package/src/types/react-native-incall-manager.d.ts +17 -0
  117. package/src/types/react-native-webrtc.d.ts +47 -0
  118. package/src/types.ts +133 -0
  119. package/src/ui/VaniraWidget.ts +87 -0
  120. package/src/ui/abstraction/AbstractWidgetProvider.ts +18 -0
  121. package/src/ui/abstraction/interfaces.ts +12 -0
  122. package/src/ui/adapters/VaniraChatAdapter.ts +42 -0
  123. package/src/ui/components/AvatarView.ts +81 -0
  124. package/src/ui/components/ChatWindow.ts +263 -0
  125. package/src/ui/components/FloatingButton.ts +163 -0
  126. package/src/ui/components/FloatingWelcomeChips.ts +137 -0
  127. package/src/ui/components/Panel.ts +120 -0
  128. package/src/ui/components/VoiceOrb.ts +79 -0
  129. package/src/ui/components/VoiceOverlay.ts +497 -0
  130. package/src/ui/components/index.ts +7 -0
  131. package/src/ui/factory/WidgetFactory.ts +16 -0
  132. package/src/ui/icons_data.ts +2 -0
  133. package/src/ui/presets/WidgetPresetRenderer.ts +1802 -0
  134. package/src/ui/presets/types.ts +16 -0
  135. package/src/ui/providers/VaniraInternalProvider.ts +1066 -0
  136. package/src/ui/styles/index.ts +323 -0
  137. package/src/ui/styles/keyframes.ts +76 -0
  138. package/src/ui/styles/theme.ts +57 -0
  139. package/src/ui/styles/widget.css.ts +838 -0
  140. package/src/ui/utils.ts +37 -0
  141. package/src/ui/views/AbstractChatView.ts +93 -0
  142. package/src/ui/views/AbstractVoiceView.ts +57 -0
  143. package/src/ui/views/AvatarOnlyView.ts +78 -0
  144. package/src/ui/views/ChatAvatarView.ts +66 -0
  145. package/src/ui/views/ChatOnlyView.ts +28 -0
  146. package/src/ui/views/ChatVoiceView.ts +15 -0
  147. package/src/ui/views/VoiceOnlyView.ts +25 -0
  148. package/src/ui/views/index.ts +5 -0
@@ -0,0 +1,457 @@
1
+ import React, {useCallback, useEffect, useMemo, useState} from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ Pressable,
5
+ ScrollView,
6
+ StyleSheet,
7
+ Text,
8
+ View,
9
+ } from 'react-native';
10
+ import {
11
+ buildCalendarCompletePayload,
12
+ buildSlotsFetchRequest,
13
+ FALLBACK_SLOTS,
14
+ formatSlotTime,
15
+ parseSlotsResponse,
16
+ slotsAreEqual,
17
+ startOfLocalDay,
18
+ type SlotItem,
19
+ } from './calendar/calendarUtils';
20
+ import {PresetShellModal} from './PresetShellModal';
21
+ import type {PresetContext} from './types';
22
+
23
+ type Props = {
24
+ visible: boolean;
25
+ ctx: PresetContext;
26
+ onClose: () => void;
27
+ onCancel: (reason?: string) => void;
28
+ onComplete: (payload: Record<string, unknown>) => void;
29
+ };
30
+
31
+ const WEEKDAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] as const;
32
+
33
+ export function BookingCalendarModal({
34
+ visible,
35
+ ctx,
36
+ onClose,
37
+ onCancel,
38
+ onComplete,
39
+ }: Props) {
40
+ const today = useMemo(() => {
41
+ const d = new Date();
42
+ d.setHours(0, 0, 0, 0);
43
+ return d;
44
+ }, []);
45
+
46
+ const mergedFields = useMemo(
47
+ () => ({...ctx.args, ...ctx.clientFields}),
48
+ [ctx.args, ctx.clientFields],
49
+ );
50
+
51
+ const title = String(mergedFields.title ?? 'Select Date & Time');
52
+ const reason = String(
53
+ ctx.args.reason ??
54
+ mergedFields.reason ??
55
+ 'Please select a preferred time for your appointment.',
56
+ );
57
+
58
+ const [viewYear, setViewYear] = useState(today.getFullYear());
59
+ const [viewMonth, setViewMonth] = useState(today.getMonth());
60
+ const [selectedTs, setSelectedTs] = useState<number | null>(null);
61
+ const [selectedTime, setSelectedTime] = useState<SlotItem | null>(null);
62
+ const [slots, setSlots] = useState<SlotItem[]>([]);
63
+ const [slotsLoading, setSlotsLoading] = useState(false);
64
+ const [slotsError, setSlotsError] = useState<string | null>(null);
65
+
66
+ useEffect(() => {
67
+ if (!visible) {
68
+ return;
69
+ }
70
+ setViewYear(today.getFullYear());
71
+ setViewMonth(today.getMonth());
72
+ setSelectedTs(null);
73
+ setSelectedTime(null);
74
+ setSlots([]);
75
+ setSlotsLoading(false);
76
+ setSlotsError(null);
77
+ }, [visible, today, ctx.toolCallId]);
78
+
79
+ const monthLabel = useMemo(
80
+ () =>
81
+ new Date(viewYear, viewMonth, 1).toLocaleDateString('en-US', {
82
+ month: 'long',
83
+ year: 'numeric',
84
+ }),
85
+ [viewYear, viewMonth],
86
+ );
87
+
88
+ const isPrevDisabled =
89
+ viewYear === today.getFullYear() && viewMonth === today.getMonth();
90
+
91
+ const goPrevMonth = useCallback(() => {
92
+ if (isPrevDisabled) {
93
+ return;
94
+ }
95
+ if (viewMonth === 0) {
96
+ setViewMonth(11);
97
+ setViewYear(y => y - 1);
98
+ } else {
99
+ setViewMonth(m => m - 1);
100
+ }
101
+ }, [isPrevDisabled, viewMonth]);
102
+
103
+ const goNextMonth = useCallback(() => {
104
+ if (viewMonth === 11) {
105
+ setViewMonth(0);
106
+ setViewYear(y => y + 1);
107
+ } else {
108
+ setViewMonth(m => m + 1);
109
+ }
110
+ }, [viewMonth]);
111
+
112
+ const loadSlotsForDate = useCallback(
113
+ async (ts: number) => {
114
+ setSelectedTime(null);
115
+ setSlots([]);
116
+ setSlotsError(null);
117
+
118
+ const request = buildSlotsFetchRequest(ts, mergedFields);
119
+ if (!request) {
120
+ setSlots([...FALLBACK_SLOTS]);
121
+ return;
122
+ }
123
+
124
+ setSlotsLoading(true);
125
+ try {
126
+ console.log('[BookingCalendarModal] slots API', request.url, request.method);
127
+ const response = await fetch(request.url, {
128
+ method: request.method,
129
+ headers: request.headers,
130
+ body: request.body,
131
+ });
132
+ if (!response.ok) {
133
+ throw new Error(`HTTP ${response.status}`);
134
+ }
135
+ const data = await response.json();
136
+ const parsed = parseSlotsResponse(data);
137
+ setSlots(parsed.length > 0 ? parsed : []);
138
+ } catch (err) {
139
+ console.warn('[BookingCalendarModal] slots fetch failed:', err);
140
+ setSlotsError('Failed to load slots. Please try again.');
141
+ setSlots([]);
142
+ } finally {
143
+ setSlotsLoading(false);
144
+ }
145
+ },
146
+ [mergedFields],
147
+ );
148
+
149
+ const selectDay = useCallback(
150
+ (ts: number) => {
151
+ setSelectedTs(ts);
152
+ loadSlotsForDate(ts);
153
+ },
154
+ [loadSlotsForDate],
155
+ );
156
+
157
+ const confirmEnabled = selectedTs !== null && selectedTime !== null;
158
+
159
+ const handleConfirm = useCallback(() => {
160
+ if (!confirmEnabled || selectedTs === null || selectedTime === null) {
161
+ return;
162
+ }
163
+ const tzStr = String(mergedFields.timezone ?? '');
164
+ const payload = buildCalendarCompletePayload(
165
+ selectedTs,
166
+ selectedTime,
167
+ tzStr,
168
+ );
169
+ onComplete(payload);
170
+ }, [confirmEnabled, mergedFields.timezone, onComplete, selectedTime, selectedTs]);
171
+
172
+ const calendarCells = useMemo(() => {
173
+ const firstDay = new Date(viewYear, viewMonth, 1).getDay();
174
+ const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
175
+ const cells: Array<{key: string; kind: 'empty' | 'day'; day?: number; ts?: number}> =
176
+ [];
177
+
178
+ for (let i = 0; i < firstDay; i++) {
179
+ cells.push({key: `empty_${i}`, kind: 'empty'});
180
+ }
181
+ for (let day = 1; day <= daysInMonth; day++) {
182
+ const ts = startOfLocalDay(viewYear, viewMonth, day);
183
+ cells.push({key: `day_${day}`, kind: 'day', day, ts});
184
+ }
185
+ return cells;
186
+ }, [viewMonth, viewYear]);
187
+
188
+ const slotsLabel =
189
+ selectedTs !== null
190
+ ? new Date(selectedTs).toLocaleDateString('en-US', {
191
+ weekday: 'long',
192
+ month: 'short',
193
+ day: 'numeric',
194
+ })
195
+ : 'Available Times';
196
+
197
+ const slotColumns =
198
+ slots.some(t => formatSlotTime(t).length > 8) ? 2 : 3;
199
+
200
+ return (
201
+ <PresetShellModal
202
+ visible={visible}
203
+ title={title}
204
+ subtitle={reason}
205
+ onClose={() => onCancel('User closed the preset')}
206
+ hideDefaultClose>
207
+ <ScrollView
208
+ style={styles.scroll}
209
+ contentContainerStyle={styles.scrollContent}
210
+ keyboardShouldPersistTaps="handled">
211
+ <View style={styles.monthNav}>
212
+ <Pressable
213
+ style={[styles.navBtn, isPrevDisabled && styles.navBtnDisabled]}
214
+ onPress={goPrevMonth}
215
+ disabled={isPrevDisabled}>
216
+ <Text style={styles.navBtnText}>‹</Text>
217
+ </Pressable>
218
+ <Text style={styles.monthLabel}>{monthLabel}</Text>
219
+ <Pressable style={styles.navBtn} onPress={goNextMonth}>
220
+ <Text style={styles.navBtnText}>›</Text>
221
+ </Pressable>
222
+ </View>
223
+
224
+ <View style={styles.weekRow}>
225
+ {WEEKDAYS.map(wd => (
226
+ <Text key={wd} style={styles.weekday}>
227
+ {wd}
228
+ </Text>
229
+ ))}
230
+ </View>
231
+
232
+ <View style={styles.grid}>
233
+ {calendarCells.map(cell => {
234
+ if (cell.kind === 'empty' || cell.ts === undefined) {
235
+ return <View key={cell.key} style={styles.dayCell} />;
236
+ }
237
+ const isPast = cell.ts < today.getTime();
238
+ const isToday = cell.ts === today.getTime();
239
+ const isSelected = selectedTs === cell.ts;
240
+ return (
241
+ <View key={cell.key} style={styles.dayCell}>
242
+ <Pressable
243
+ style={[
244
+ styles.dayBtn,
245
+ isPast && styles.dayPast,
246
+ isToday && !isSelected && styles.dayToday,
247
+ isSelected && styles.daySelected,
248
+ ]}
249
+ disabled={isPast}
250
+ onPress={() => selectDay(cell.ts!)}>
251
+ <Text
252
+ style={[
253
+ styles.dayText,
254
+ isPast && styles.dayTextPast,
255
+ isSelected && styles.dayTextSelected,
256
+ ]}>
257
+ {cell.day}
258
+ </Text>
259
+ </Pressable>
260
+ </View>
261
+ );
262
+ })}
263
+ </View>
264
+
265
+ {selectedTs !== null ? (
266
+ <View style={styles.slotsSection}>
267
+ <View style={styles.divider} />
268
+ <Text style={styles.slotsLabel}>{slotsLabel}</Text>
269
+
270
+ {slotsLoading ? (
271
+ <View style={styles.centered}>
272
+ <ActivityIndicator color="#4f46e5" />
273
+ <Text style={styles.loadingText}>Loading slots…</Text>
274
+ </View>
275
+ ) : null}
276
+
277
+ {slotsError ? (
278
+ <Text style={styles.errorText}>{slotsError}</Text>
279
+ ) : null}
280
+
281
+ {!slotsLoading && slots.length === 0 ? (
282
+ <Text style={styles.emptySlots}>
283
+ No slots available for this date.
284
+ </Text>
285
+ ) : null}
286
+
287
+ {!slotsLoading && slots.length > 0 ? (
288
+ <View style={styles.slotsGrid}>
289
+ {slots.map((slot, index) => {
290
+ const label = formatSlotTime(slot);
291
+ const isSel = slotsAreEqual(slot, selectedTime);
292
+ return (
293
+ <Pressable
294
+ key={`${label}_${index}`}
295
+ style={[
296
+ styles.slotBtn,
297
+ slotColumns === 2 ? styles.slotBtnCol2 : styles.slotBtnCol3,
298
+ isSel && styles.slotBtnSelected,
299
+ ]}
300
+ onPress={() => setSelectedTime(slot)}>
301
+ <Text
302
+ style={[
303
+ styles.slotText,
304
+ isSel && styles.slotTextSelected,
305
+ ]}>
306
+ {label}
307
+ </Text>
308
+ </Pressable>
309
+ );
310
+ })}
311
+ </View>
312
+ ) : null}
313
+ </View>
314
+ ) : null}
315
+ </ScrollView>
316
+
317
+ <View style={styles.actions}>
318
+ <Pressable
319
+ style={styles.cancelBtn}
320
+ onPress={() => onCancel('User cancelled')}>
321
+ <Text style={styles.cancelBtnText}>Cancel</Text>
322
+ </Pressable>
323
+ <Pressable
324
+ style={[styles.confirmBtn, !confirmEnabled && styles.confirmDisabled]}
325
+ onPress={handleConfirm}
326
+ disabled={!confirmEnabled}>
327
+ <Text style={styles.confirmBtnText}>Confirm</Text>
328
+ </Pressable>
329
+ </View>
330
+ </PresetShellModal>
331
+ );
332
+ }
333
+
334
+ const styles = StyleSheet.create({
335
+ scroll: {maxHeight: 420},
336
+ scrollContent: {paddingBottom: 8},
337
+ monthNav: {
338
+ flexDirection: 'row',
339
+ alignItems: 'center',
340
+ justifyContent: 'space-between',
341
+ marginBottom: 12,
342
+ },
343
+ navBtn: {
344
+ width: 36,
345
+ height: 36,
346
+ borderRadius: 8,
347
+ backgroundColor: '#f3f4f6',
348
+ alignItems: 'center',
349
+ justifyContent: 'center',
350
+ },
351
+ navBtnDisabled: {opacity: 0.35},
352
+ navBtnText: {fontSize: 22, color: '#111', lineHeight: 24},
353
+ monthLabel: {fontSize: 16, fontWeight: '600', color: '#111'},
354
+ weekRow: {
355
+ flexDirection: 'row',
356
+ marginBottom: 6,
357
+ },
358
+ weekday: {
359
+ flex: 1,
360
+ textAlign: 'center',
361
+ fontSize: 11,
362
+ fontWeight: '600',
363
+ color: '#9ca3af',
364
+ },
365
+ grid: {
366
+ flexDirection: 'row',
367
+ flexWrap: 'wrap',
368
+ },
369
+ dayCell: {
370
+ width: '14.28%',
371
+ alignItems: 'center',
372
+ marginBottom: 6,
373
+ },
374
+ dayBtn: {
375
+ width: 34,
376
+ height: 34,
377
+ borderRadius: 17,
378
+ alignItems: 'center',
379
+ justifyContent: 'center',
380
+ },
381
+ dayPast: {opacity: 0.35},
382
+ dayToday: {borderWidth: 1, borderColor: '#4f46e5'},
383
+ daySelected: {backgroundColor: '#4f46e5'},
384
+ dayText: {fontSize: 14, color: '#111', fontWeight: '500'},
385
+ dayTextPast: {color: '#9ca3af'},
386
+ dayTextSelected: {color: '#fff', fontWeight: '700'},
387
+ slotsSection: {marginTop: 8},
388
+ divider: {
389
+ height: 1,
390
+ backgroundColor: '#e5e7eb',
391
+ marginVertical: 12,
392
+ },
393
+ slotsLabel: {
394
+ fontSize: 13,
395
+ fontWeight: '600',
396
+ color: '#374151',
397
+ marginBottom: 10,
398
+ },
399
+ centered: {alignItems: 'center', paddingVertical: 16, gap: 8},
400
+ loadingText: {fontSize: 13, color: '#6b7280'},
401
+ errorText: {
402
+ fontSize: 13,
403
+ color: '#ef4444',
404
+ textAlign: 'center',
405
+ marginBottom: 8,
406
+ },
407
+ emptySlots: {
408
+ fontSize: 13,
409
+ color: '#6b7280',
410
+ textAlign: 'center',
411
+ paddingVertical: 12,
412
+ },
413
+ slotsGrid: {
414
+ flexDirection: 'row',
415
+ flexWrap: 'wrap',
416
+ gap: 8,
417
+ },
418
+ slotBtn: {
419
+ paddingVertical: 10,
420
+ paddingHorizontal: 8,
421
+ borderRadius: 8,
422
+ borderWidth: 1,
423
+ borderColor: '#e5e7eb',
424
+ backgroundColor: '#f9fafb',
425
+ alignItems: 'center',
426
+ },
427
+ slotBtnCol3: {width: '31%'},
428
+ slotBtnCol2: {width: '48%'},
429
+ slotBtnSelected: {
430
+ backgroundColor: '#4f46e5',
431
+ borderColor: '#4f46e5',
432
+ },
433
+ slotText: {fontSize: 12, color: '#111', fontWeight: '500'},
434
+ slotTextSelected: {color: '#fff'},
435
+ actions: {
436
+ flexDirection: 'row',
437
+ gap: 10,
438
+ marginTop: 12,
439
+ },
440
+ cancelBtn: {
441
+ flex: 1,
442
+ paddingVertical: 12,
443
+ borderRadius: 10,
444
+ backgroundColor: '#f3f4f6',
445
+ alignItems: 'center',
446
+ },
447
+ cancelBtnText: {color: '#111', fontWeight: '600'},
448
+ confirmBtn: {
449
+ flex: 1,
450
+ paddingVertical: 12,
451
+ borderRadius: 10,
452
+ backgroundColor: '#4f46e5',
453
+ alignItems: 'center',
454
+ },
455
+ confirmDisabled: {opacity: 0.45},
456
+ confirmBtnText: {color: '#fff', fontWeight: '700'},
457
+ });