@billdog.io/react-native 1.0.0-beta.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.
- package/BillDogReactNative.podspec +30 -0
- package/LICENSE +21 -0
- package/README.md +330 -0
- package/android/build.gradle +99 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogABTestModule.kt +317 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogAnalyticsModule.kt +389 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogModule.kt +817 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogPaywallViewManager.kt +383 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogSurveyModule.kt +427 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogVirtualCurrencyModule.kt +407 -0
- package/android/src/main/kotlin/io/billdog/virtualcurrency/BillDogVirtualCurrencyPackage.kt +44 -0
- package/dist/AdvancedPaywallCache.d.ts +70 -0
- package/dist/AdvancedPaywallCache.js +344 -0
- package/dist/BillDog.d.ts +255 -0
- package/dist/BillDog.js +918 -0
- package/dist/BillDogConfiguration.d.ts +145 -0
- package/dist/BillDogConfiguration.js +229 -0
- package/dist/ComponentRenderer.d.ts +17 -0
- package/dist/ComponentRenderer.js +886 -0
- package/dist/LayoutEngine.d.ts +101 -0
- package/dist/LayoutEngine.js +394 -0
- package/dist/LocalizationResolver.d.ts +99 -0
- package/dist/LocalizationResolver.js +266 -0
- package/dist/NativePaywallView.d.ts +36 -0
- package/dist/NativePaywallView.js +59 -0
- package/dist/PaywallCache.d.ts +74 -0
- package/dist/PaywallCache.js +293 -0
- package/dist/PaywallStateManager.d.ts +62 -0
- package/dist/PaywallStateManager.js +147 -0
- package/dist/PaywallView.d.ts +42 -0
- package/dist/PaywallView.js +275 -0
- package/dist/ProductVariableProvider.d.ts +60 -0
- package/dist/ProductVariableProvider.js +239 -0
- package/dist/VariableResolver.d.ts +120 -0
- package/dist/VariableResolver.js +379 -0
- package/dist/WebViewPaywall.d.ts +75 -0
- package/dist/WebViewPaywall.js +360 -0
- package/dist/animations/presets.d.ts +49 -0
- package/dist/animations/presets.js +367 -0
- package/dist/components/AccoladeCpsComponent.d.ts +13 -0
- package/dist/components/AccoladeCpsComponent.js +248 -0
- package/dist/components/AccordionComponent.d.ts +10 -0
- package/dist/components/AccordionComponent.js +187 -0
- package/dist/components/AnimatedWrapper.d.ts +35 -0
- package/dist/components/AnimatedWrapper.js +277 -0
- package/dist/components/CarouselComponent.d.ts +23 -0
- package/dist/components/CarouselComponent.js +484 -0
- package/dist/components/ChartCurveComponent.d.ts +13 -0
- package/dist/components/ChartCurveComponent.js +333 -0
- package/dist/components/CheckboxComponent.d.ts +9 -0
- package/dist/components/CheckboxComponent.js +172 -0
- package/dist/components/ConfettiComponent.d.ts +9 -0
- package/dist/components/ConfettiComponent.js +172 -0
- package/dist/components/CountdownComponent.d.ts +16 -0
- package/dist/components/CountdownComponent.js +414 -0
- package/dist/components/DelayedXComponent.d.ts +12 -0
- package/dist/components/DelayedXComponent.js +240 -0
- package/dist/components/DrawerModalComponent.d.ts +20 -0
- package/dist/components/DrawerModalComponent.js +286 -0
- package/dist/components/GradientView.d.ts +38 -0
- package/dist/components/GradientView.js +126 -0
- package/dist/components/HapticFeedbackComponent.d.ts +16 -0
- package/dist/components/HapticFeedbackComponent.js +64 -0
- package/dist/components/HorizontalProductsComponent.d.ts +17 -0
- package/dist/components/HorizontalProductsComponent.js +284 -0
- package/dist/components/MultiChoiceGridComponent.d.ts +13 -0
- package/dist/components/MultiChoiceGridComponent.js +234 -0
- package/dist/components/NPSScaleComponent.d.ts +16 -0
- package/dist/components/NPSScaleComponent.js +260 -0
- package/dist/components/NavigationComponent.d.ts +26 -0
- package/dist/components/NavigationComponent.js +302 -0
- package/dist/components/OAuthButtonComponent.d.ts +20 -0
- package/dist/components/OAuthButtonComponent.js +237 -0
- package/dist/components/ProgressLoaderComponent.d.ts +13 -0
- package/dist/components/ProgressLoaderComponent.js +308 -0
- package/dist/components/ProgressStepperComponent.d.ts +18 -0
- package/dist/components/ProgressStepperComponent.js +228 -0
- package/dist/components/SegmentedPickerComponent.d.ts +10 -0
- package/dist/components/SegmentedPickerComponent.js +221 -0
- package/dist/components/ShareButtonsComponent.d.ts +21 -0
- package/dist/components/ShareButtonsComponent.js +265 -0
- package/dist/components/ShareCodeComponent.d.ts +16 -0
- package/dist/components/ShareCodeComponent.js +274 -0
- package/dist/components/SingleChoiceGridComponent.d.ts +13 -0
- package/dist/components/SingleChoiceGridComponent.js +210 -0
- package/dist/components/SliderComponent.d.ts +9 -0
- package/dist/components/SliderComponent.js +263 -0
- package/dist/components/StarRatingInputComponent.d.ts +20 -0
- package/dist/components/StarRatingInputComponent.js +198 -0
- package/dist/components/StickerCompositeComponent.d.ts +13 -0
- package/dist/components/StickerCompositeComponent.js +136 -0
- package/dist/components/TabsComponent.d.ts +19 -0
- package/dist/components/TabsComponent.js +431 -0
- package/dist/components/TextInputComponent.d.ts +18 -0
- package/dist/components/TextInputComponent.js +227 -0
- package/dist/components/TimelineComponent.d.ts +19 -0
- package/dist/components/TimelineComponent.js +104 -0
- package/dist/components/ToggleComponent.d.ts +14 -0
- package/dist/components/ToggleComponent.js +132 -0
- package/dist/components/VerticalProductsComponent.d.ts +17 -0
- package/dist/components/VerticalProductsComponent.js +311 -0
- package/dist/dynamicValues.d.ts +36 -0
- package/dist/dynamicValues.js +142 -0
- package/dist/filters/dateFilters.d.ts +50 -0
- package/dist/filters/dateFilters.js +390 -0
- package/dist/filters/index.d.ts +18 -0
- package/dist/filters/index.js +253 -0
- package/dist/hooks/useOfferings.d.ts +76 -0
- package/dist/hooks/useOfferings.js +99 -0
- package/dist/hooks/usePaywall.d.ts +27 -0
- package/dist/hooks/usePaywall.js +114 -0
- package/dist/hooks/usePlacement.d.ts +34 -0
- package/dist/hooks/usePlacement.js +89 -0
- package/dist/hooks/useSubscription.d.ts +40 -0
- package/dist/hooks/useSubscription.js +97 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +198 -0
- package/dist/models/BillDogError.d.ts +40 -0
- package/dist/models/BillDogError.js +11 -0
- package/dist/models/CacheFetchPolicy.d.ts +8 -0
- package/dist/models/CacheFetchPolicy.js +4 -0
- package/dist/models/CustomProduct.d.ts +32 -0
- package/dist/models/CustomProduct.js +15 -0
- package/dist/models/CustomerInfo.d.ts +17 -0
- package/dist/models/CustomerInfo.js +11 -0
- package/dist/models/Delegate.d.ts +20 -0
- package/dist/models/Delegate.js +11 -0
- package/dist/models/EntitlementInfo.d.ts +19 -0
- package/dist/models/EntitlementInfo.js +2 -0
- package/dist/models/Placement.d.ts +54 -0
- package/dist/models/Placement.js +15 -0
- package/dist/models/PurchaseParams.d.ts +39 -0
- package/dist/models/PurchaseParams.js +60 -0
- package/dist/models/PurchaseResult.d.ts +30 -0
- package/dist/models/PurchaseResult.js +15 -0
- package/dist/models/RestoreResult.d.ts +30 -0
- package/dist/models/RestoreResult.js +22 -0
- package/dist/models/RestoredItem.d.ts +26 -0
- package/dist/models/RestoredItem.js +13 -0
- package/dist/modules/ABTest.d.ts +65 -0
- package/dist/modules/ABTest.js +113 -0
- package/dist/modules/Analytics.d.ts +91 -0
- package/dist/modules/Analytics.js +182 -0
- package/dist/modules/Survey.d.ts +65 -0
- package/dist/modules/Survey.js +82 -0
- package/dist/modules/VirtualCurrency.d.ts +63 -0
- package/dist/modules/VirtualCurrency.js +147 -0
- package/dist/murmur.d.ts +23 -0
- package/dist/murmur.js +104 -0
- package/dist/notifications/LiveActivitiesManager.d.ts +47 -0
- package/dist/notifications/LiveActivitiesManager.js +106 -0
- package/dist/notifications/NotificationScheduler.d.ts +55 -0
- package/dist/notifications/NotificationScheduler.js +199 -0
- package/dist/notifications/PurchaseNotificationHandler.d.ts +24 -0
- package/dist/notifications/PurchaseNotificationHandler.js +55 -0
- package/dist/notifications/useDeviceToken.d.ts +36 -0
- package/dist/notifications/useDeviceToken.js +97 -0
- package/dist/notifications/useLiveActivity.d.ts +39 -0
- package/dist/notifications/useLiveActivity.js +104 -0
- package/dist/review/ReviewAnalytics.d.ts +34 -0
- package/dist/review/ReviewAnalytics.js +73 -0
- package/dist/review/ReviewManager.d.ts +83 -0
- package/dist/review/ReviewManager.js +339 -0
- package/dist/review/ReviewTimingManager.d.ts +82 -0
- package/dist/review/ReviewTimingManager.js +224 -0
- package/dist/survey-core/types.d.ts +215 -0
- package/dist/survey-core/types.js +8 -0
- package/dist/types.d.ts +438 -0
- package/dist/types.js +158 -0
- package/dist/utils/ApiClient.d.ts +65 -0
- package/dist/utils/ApiClient.js +132 -0
- package/dist/utils/CSSConverter.d.ts +49 -0
- package/dist/utils/CSSConverter.js +470 -0
- package/dist/utils/animations.d.ts +41 -0
- package/dist/utils/animations.js +194 -0
- package/dist/utils/calendarIntegration.d.ts +36 -0
- package/dist/utils/calendarIntegration.js +89 -0
- package/dist/utils/configurationResolver.d.ts +23 -0
- package/dist/utils/configurationResolver.js +78 -0
- package/dist/utils/deviceDetection.d.ts +38 -0
- package/dist/utils/deviceDetection.js +59 -0
- package/dist/utils/hapticFeedback.d.ts +17 -0
- package/dist/utils/hapticFeedback.js +53 -0
- package/dist/utils/iconMapper.d.ts +26 -0
- package/dist/utils/iconMapper.js +113 -0
- package/dist/utils/notificationPermissions.d.ts +13 -0
- package/dist/utils/notificationPermissions.js +51 -0
- package/dist/utils/priceFormatting.d.ts +19 -0
- package/dist/utils/priceFormatting.js +99 -0
- package/dist/utils/shareSheet.d.ts +17 -0
- package/dist/utils/shareSheet.js +42 -0
- package/dist/utils/subscriptionUpgrade.d.ts +84 -0
- package/dist/utils/subscriptionUpgrade.js +100 -0
- package/dist/valueUtils.d.ts +48 -0
- package/dist/valueUtils.js +129 -0
- package/dist/xFlowTags.d.ts +18 -0
- package/dist/xFlowTags.js +385 -0
- package/ios/BillDogABTest.m +50 -0
- package/ios/BillDogABTest.swift +188 -0
- package/ios/BillDogAnalytics.m +105 -0
- package/ios/BillDogAnalytics.swift +324 -0
- package/ios/BillDogBridge.m +222 -0
- package/ios/BillDogBridge.swift +715 -0
- package/ios/BillDogLiveActivities.m +41 -0
- package/ios/BillDogLiveActivities.swift +104 -0
- package/ios/BillDogPaywallViewManager.m +39 -0
- package/ios/BillDogPaywallViewManager.swift +361 -0
- package/ios/BillDogSurvey.m +34 -0
- package/ios/BillDogSurvey.swift +257 -0
- package/ios/BillDogVirtualCurrencyBridge.m +63 -0
- package/ios/BillDogVirtualCurrencyBridge.swift +511 -0
- package/junit.xml +25 -0
- package/package.json +61 -0
- package/react-native.config.js +13 -0
- package/src/AdvancedPaywallCache.ts +433 -0
- package/src/BillDog.ts +983 -0
- package/src/BillDogConfiguration.ts +318 -0
- package/src/ComponentRenderer.tsx +1325 -0
- package/src/LayoutEngine.ts +459 -0
- package/src/LocalizationResolver.ts +311 -0
- package/src/NativePaywallView.tsx +132 -0
- package/src/PaywallCache.ts +358 -0
- package/src/PaywallStateManager.ts +154 -0
- package/src/PaywallView.tsx +328 -0
- package/src/ProductVariableProvider.ts +282 -0
- package/src/VariableResolver.ts +516 -0
- package/src/WebViewPaywall.tsx +454 -0
- package/src/animations/presets.ts +421 -0
- package/src/components/AccoladeCpsComponent.tsx +259 -0
- package/src/components/AccordionComponent.tsx +213 -0
- package/src/components/AnimatedWrapper.tsx +316 -0
- package/src/components/CarouselComponent.tsx +615 -0
- package/src/components/ChartCurveComponent.tsx +452 -0
- package/src/components/CheckboxComponent.tsx +185 -0
- package/src/components/ConfettiComponent.tsx +178 -0
- package/src/components/CountdownComponent.tsx +454 -0
- package/src/components/DelayedXComponent.tsx +266 -0
- package/src/components/DrawerModalComponent.tsx +324 -0
- package/src/components/GradientView.tsx +129 -0
- package/src/components/HapticFeedbackComponent.tsx +39 -0
- package/src/components/HorizontalProductsComponent.tsx +329 -0
- package/src/components/MultiChoiceGridComponent.tsx +265 -0
- package/src/components/NPSScaleComponent.tsx +294 -0
- package/src/components/NavigationComponent.tsx +329 -0
- package/src/components/OAuthButtonComponent.tsx +266 -0
- package/src/components/ProgressLoaderComponent.tsx +366 -0
- package/src/components/ProgressStepperComponent.tsx +250 -0
- package/src/components/SegmentedPickerComponent.tsx +245 -0
- package/src/components/ShareButtonsComponent.tsx +299 -0
- package/src/components/ShareCodeComponent.tsx +303 -0
- package/src/components/SingleChoiceGridComponent.tsx +229 -0
- package/src/components/SliderComponent.tsx +280 -0
- package/src/components/StarRatingInputComponent.tsx +223 -0
- package/src/components/StickerCompositeComponent.tsx +168 -0
- package/src/components/TabsComponent.tsx +496 -0
- package/src/components/TextInputComponent.tsx +258 -0
- package/src/components/TimelineComponent.tsx +129 -0
- package/src/components/ToggleComponent.tsx +142 -0
- package/src/components/VerticalProductsComponent.tsx +357 -0
- package/src/dynamicValues.ts +180 -0
- package/src/filters/dateFilters.ts +411 -0
- package/src/filters/index.ts +281 -0
- package/src/hooks/useOfferings.ts +192 -0
- package/src/hooks/usePaywall.ts +154 -0
- package/src/hooks/usePlacement.ts +143 -0
- package/src/hooks/useSubscription.ts +154 -0
- package/src/index.ts +245 -0
- package/src/models/BillDogError.ts +24 -0
- package/src/models/CacheFetchPolicy.ts +13 -0
- package/src/models/CustomProduct.ts +33 -0
- package/src/models/CustomerInfo.ts +26 -0
- package/src/models/Delegate.ts +22 -0
- package/src/models/EntitlementInfo.ts +27 -0
- package/src/models/Placement.ts +50 -0
- package/src/models/PurchaseParams.ts +81 -0
- package/src/models/PurchaseResult.ts +32 -0
- package/src/models/RestoreResult.ts +27 -0
- package/src/models/RestoredItem.ts +25 -0
- package/src/modules/ABTest.ts +131 -0
- package/src/modules/Analytics.ts +227 -0
- package/src/modules/Survey.ts +126 -0
- package/src/modules/VirtualCurrency.ts +223 -0
- package/src/murmur.ts +102 -0
- package/src/notifications/LiveActivitiesManager.ts +140 -0
- package/src/notifications/NotificationScheduler.ts +292 -0
- package/src/notifications/PurchaseNotificationHandler.ts +67 -0
- package/src/notifications/useDeviceToken.ts +116 -0
- package/src/notifications/useLiveActivity.ts +125 -0
- package/src/review/ReviewAnalytics.ts +89 -0
- package/src/review/ReviewManager.ts +355 -0
- package/src/review/ReviewTimingManager.ts +265 -0
- package/src/survey-core/types.ts +244 -0
- package/src/types/ambient.d.ts +9 -0
- package/src/types.ts +680 -0
- package/src/utils/ApiClient.ts +163 -0
- package/src/utils/CSSConverter.ts +552 -0
- package/src/utils/animations.ts +232 -0
- package/src/utils/calendarIntegration.ts +113 -0
- package/src/utils/configurationResolver.ts +106 -0
- package/src/utils/deviceDetection.ts +83 -0
- package/src/utils/hapticFeedback.ts +55 -0
- package/src/utils/iconMapper.ts +102 -0
- package/src/utils/notificationPermissions.ts +48 -0
- package/src/utils/priceFormatting.ts +119 -0
- package/src/utils/shareSheet.ts +55 -0
- package/src/utils/subscriptionUpgrade.ts +183 -0
- package/src/valueUtils.ts +108 -0
- package/src/xFlowTags.ts +463 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TabsComponent - Interactive tabs for React Native
|
|
3
|
+
* Uses element:{id}.currentIndex for state tracking (Superwall pattern)
|
|
4
|
+
* Full property support matching web schema for cross-platform consistency
|
|
5
|
+
* Phase 2 Enhanced: icon support, alignment, sizing, disabled state, animation duration, overflow
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
9
|
+
import {
|
|
10
|
+
View,
|
|
11
|
+
Text,
|
|
12
|
+
TouchableOpacity,
|
|
13
|
+
StyleSheet,
|
|
14
|
+
LayoutAnimation,
|
|
15
|
+
Platform,
|
|
16
|
+
UIManager,
|
|
17
|
+
ViewStyle,
|
|
18
|
+
TextStyle,
|
|
19
|
+
ScrollView,
|
|
20
|
+
} from 'react-native';
|
|
21
|
+
import type { PaywallComponent } from '../types';
|
|
22
|
+
|
|
23
|
+
// Enable LayoutAnimation on Android
|
|
24
|
+
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
|
|
25
|
+
UIManager.setLayoutAnimationEnabledExperimental(true);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Icon mapping for React Native (using basic Unicode symbols as fallback)
|
|
29
|
+
const ICON_MAP: Record<string, string> = {
|
|
30
|
+
home: '🏠',
|
|
31
|
+
star: '⭐',
|
|
32
|
+
settings: '⚙️',
|
|
33
|
+
person: '👤',
|
|
34
|
+
favorite: '❤️',
|
|
35
|
+
heart: '❤️',
|
|
36
|
+
search: '🔍',
|
|
37
|
+
check: '✓',
|
|
38
|
+
close: '✕',
|
|
39
|
+
add: '+',
|
|
40
|
+
remove: '-',
|
|
41
|
+
edit: '✏️',
|
|
42
|
+
delete: '🗑️',
|
|
43
|
+
share: '↗️',
|
|
44
|
+
download: '⬇️',
|
|
45
|
+
upload: '⬆️',
|
|
46
|
+
refresh: '🔄',
|
|
47
|
+
info: 'ℹ️',
|
|
48
|
+
help: '❓',
|
|
49
|
+
warning: '⚠️',
|
|
50
|
+
error: '❌',
|
|
51
|
+
lock: '🔒',
|
|
52
|
+
unlock: '🔓',
|
|
53
|
+
notification: '🔔',
|
|
54
|
+
email: '✉️',
|
|
55
|
+
phone: '📞',
|
|
56
|
+
location: '📍',
|
|
57
|
+
calendar: '📅',
|
|
58
|
+
time: '⏰',
|
|
59
|
+
camera: '📷',
|
|
60
|
+
photo: '🖼️',
|
|
61
|
+
video: '🎥',
|
|
62
|
+
music: '🎵',
|
|
63
|
+
play: '▶️',
|
|
64
|
+
pause: '⏸️',
|
|
65
|
+
stop: '⏹️',
|
|
66
|
+
crown: '👑',
|
|
67
|
+
premium: '👑',
|
|
68
|
+
diamond: '💎',
|
|
69
|
+
gift: '🎁',
|
|
70
|
+
cart: '🛒',
|
|
71
|
+
wallet: '💰',
|
|
72
|
+
rocket: '🚀',
|
|
73
|
+
trophy: '🏆',
|
|
74
|
+
medal: '🏅',
|
|
75
|
+
badge: '📛',
|
|
76
|
+
verified: '✅',
|
|
77
|
+
shield: '🛡️',
|
|
78
|
+
bolt: '⚡',
|
|
79
|
+
fire: '🔥',
|
|
80
|
+
sparkle: '✨',
|
|
81
|
+
magic: '🪄',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
interface TabsComponentProps {
|
|
85
|
+
component: PaywallComponent;
|
|
86
|
+
/** Child components to render within the tabs (defaults to component.children) */
|
|
87
|
+
children?: PaywallComponent[];
|
|
88
|
+
renderChild: (child: PaywallComponent, index: number) => React.ReactNode;
|
|
89
|
+
stateManager?: any;
|
|
90
|
+
isPreviewMode?: boolean;
|
|
91
|
+
onTabChange?: (componentId: string, previousIndex: number, newIndex: number, tabLabel: string) => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const TabsComponent: React.FC<TabsComponentProps> = ({
|
|
95
|
+
component,
|
|
96
|
+
renderChild,
|
|
97
|
+
stateManager,
|
|
98
|
+
isPreviewMode = false,
|
|
99
|
+
onTabChange,
|
|
100
|
+
}) => {
|
|
101
|
+
const props = component.properties || {};
|
|
102
|
+
|
|
103
|
+
// Core Properties
|
|
104
|
+
const tabCount = (props.tabCount as number) ?? 3;
|
|
105
|
+
const defaultTab = (props.defaultTab as number) ?? 0;
|
|
106
|
+
const animated = (props.animated as boolean) ?? true;
|
|
107
|
+
const variant = (props.variant as string) ?? 'pills';
|
|
108
|
+
|
|
109
|
+
// Phase 2: New Properties
|
|
110
|
+
const tabAlignment = (props.tabAlignment as string) ?? 'stretch';
|
|
111
|
+
const tabSizing = (props.tabSizing as string) ?? 'equal';
|
|
112
|
+
const fixedTabWidth = props.fixedTabWidth as number | undefined;
|
|
113
|
+
const animationDuration = (props.animationDuration as number) ?? 200;
|
|
114
|
+
const overflowBehavior = (props.overflowBehavior as string) ?? 'scroll';
|
|
115
|
+
const tabIconPosition = (props.tabIconPosition as string) ?? 'left';
|
|
116
|
+
|
|
117
|
+
// Styling Properties (matching web schema)
|
|
118
|
+
const activeTabColor = (props.activeTabColor as string) ?? '#FFFFFF';
|
|
119
|
+
const inactiveTabColor = (props.inactiveTabColor as string) ?? 'transparent';
|
|
120
|
+
const activeTextColor = (props.activeTextColor as string) ?? '#000000';
|
|
121
|
+
const inactiveTextColor = (props.inactiveTextColor as string) ?? '#666666';
|
|
122
|
+
const tabListBackgroundColor = (props.tabListBackgroundColor as string) ?? '#F5F5F5';
|
|
123
|
+
const tabFontSize = (props.tabFontSize as number) ?? 14;
|
|
124
|
+
const tabFontWeight = (props.tabFontWeight as string) ?? '600';
|
|
125
|
+
const tabBorderRadius = (props.tabBorderRadius as number) ?? 8;
|
|
126
|
+
const tabListBorderRadius = (props.tabListBorderRadius as number) ?? 10;
|
|
127
|
+
const tabGap = (props.tabGap as number) ?? 4;
|
|
128
|
+
const tabPaddingX = (props.tabPaddingX as number) ?? 16;
|
|
129
|
+
const tabPaddingY = (props.tabPaddingY as number) ?? 10;
|
|
130
|
+
|
|
131
|
+
// Phase 2: Disabled state colors
|
|
132
|
+
const disabledTextColor = (props.disabledTextColor as string) ?? '#AAAAAA';
|
|
133
|
+
const disabledTabFill = (props.disabledTabFill as string) ?? '#F0F0F0';
|
|
134
|
+
|
|
135
|
+
const [currentIndex, setCurrentIndex] = useState(defaultTab);
|
|
136
|
+
|
|
137
|
+
// Resolve tab children by role (canonical pattern matching all other renderers)
|
|
138
|
+
const tabChildren = useMemo(() => {
|
|
139
|
+
return (component.children || [])
|
|
140
|
+
.filter(child => /^tab-\d+$/.test(child.role || ''))
|
|
141
|
+
.sort((a, b) => {
|
|
142
|
+
const aNum = parseInt(a.role?.replace('tab-', '') || '0');
|
|
143
|
+
const bNum = parseInt(b.role?.replace('tab-', '') || '0');
|
|
144
|
+
return aNum - bNum;
|
|
145
|
+
});
|
|
146
|
+
}, [component.children]);
|
|
147
|
+
|
|
148
|
+
// Get tab label from child properties (canonical: label → text → name)
|
|
149
|
+
const getTabLabel = useCallback((index: number): string => {
|
|
150
|
+
const child = tabChildren[index];
|
|
151
|
+
if (!child) return `Tab ${index + 1}`;
|
|
152
|
+
return (child.properties?.label as string)
|
|
153
|
+
?? (child.properties?.text as string)
|
|
154
|
+
?? (child.name as string)
|
|
155
|
+
?? `Tab ${index + 1}`;
|
|
156
|
+
}, [tabChildren]);
|
|
157
|
+
|
|
158
|
+
// Get tab icon from child properties
|
|
159
|
+
const getTabIcon = useCallback((index: number): string | undefined => {
|
|
160
|
+
const child = tabChildren[index];
|
|
161
|
+
const iconName = child?.properties?.icon as string | undefined;
|
|
162
|
+
if (iconName && ICON_MAP[iconName.toLowerCase()]) {
|
|
163
|
+
return ICON_MAP[iconName.toLowerCase()];
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}, [tabChildren]);
|
|
167
|
+
|
|
168
|
+
// Check if tab is disabled from child properties
|
|
169
|
+
const isTabDisabled = useCallback((index: number): boolean => {
|
|
170
|
+
const child = tabChildren[index];
|
|
171
|
+
return (child?.properties?.disabled as boolean) ?? false;
|
|
172
|
+
}, [tabChildren]);
|
|
173
|
+
|
|
174
|
+
// Parse children to find content panels - filter by content-panel-X role and sort
|
|
175
|
+
const contentPanels = useMemo(() => {
|
|
176
|
+
const children = component.children || [];
|
|
177
|
+
return children
|
|
178
|
+
.filter(child => /^content-panel-\d+$/.test(child.role || ''))
|
|
179
|
+
.sort((a, b) => {
|
|
180
|
+
const aNum = parseInt(a.role?.replace('content-panel-', '') || '0');
|
|
181
|
+
const bNum = parseInt(b.role?.replace('content-panel-', '') || '0');
|
|
182
|
+
return aNum - bNum;
|
|
183
|
+
});
|
|
184
|
+
}, [component.children]);
|
|
185
|
+
|
|
186
|
+
// Update state manager
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (stateManager) {
|
|
189
|
+
stateManager.updateState(`element:${component.id}.currentIndex`, currentIndex);
|
|
190
|
+
}
|
|
191
|
+
}, [component.id, currentIndex, stateManager]);
|
|
192
|
+
|
|
193
|
+
// Listen for external state changes
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (!stateManager) return;
|
|
196
|
+
|
|
197
|
+
const unsubscribe = stateManager.subscribeToState(
|
|
198
|
+
`element:${component.id}.currentIndex`,
|
|
199
|
+
(value: number) => {
|
|
200
|
+
if (typeof value === 'number' && value !== currentIndex) {
|
|
201
|
+
selectTab(value);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
return unsubscribe;
|
|
207
|
+
}, [component.id, currentIndex, stateManager]);
|
|
208
|
+
|
|
209
|
+
const selectTab = useCallback((index: number) => {
|
|
210
|
+
if (index < 0 || index >= tabCount) return;
|
|
211
|
+
|
|
212
|
+
// Phase 2: Check if tab is disabled
|
|
213
|
+
if (isTabDisabled(index)) return;
|
|
214
|
+
|
|
215
|
+
const previousIndex = currentIndex;
|
|
216
|
+
const tabLabel = getTabLabel(index);
|
|
217
|
+
|
|
218
|
+
if (animated) {
|
|
219
|
+
LayoutAnimation.configureNext({
|
|
220
|
+
duration: animationDuration,
|
|
221
|
+
update: { type: LayoutAnimation.Types.easeInEaseOut },
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
setCurrentIndex(index);
|
|
225
|
+
|
|
226
|
+
// Phase 2: Fire tab change callback
|
|
227
|
+
onTabChange?.(component.id, previousIndex, index, tabLabel);
|
|
228
|
+
}, [animated, animationDuration, tabCount, currentIndex, isTabDisabled, getTabLabel, onTabChange, component.id]);
|
|
229
|
+
|
|
230
|
+
// Content gap from containerLayout (matching web schema)
|
|
231
|
+
const contentGap = (component.containerLayout?.gap as number) ?? 12;
|
|
232
|
+
|
|
233
|
+
// Phase 2: Get flex justification based on alignment
|
|
234
|
+
const getJustifyContent = (): ViewStyle['justifyContent'] => {
|
|
235
|
+
switch (tabAlignment) {
|
|
236
|
+
case 'start': return 'flex-start';
|
|
237
|
+
case 'center': return 'center';
|
|
238
|
+
case 'end': return 'flex-end';
|
|
239
|
+
case 'stretch':
|
|
240
|
+
default: return 'flex-start';
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Variant-specific styles
|
|
245
|
+
const getTabListStyle = (): ViewStyle => {
|
|
246
|
+
const baseStyle: ViewStyle = {
|
|
247
|
+
backgroundColor: tabListBackgroundColor,
|
|
248
|
+
flexDirection: 'row',
|
|
249
|
+
padding: 4,
|
|
250
|
+
marginBottom: contentGap,
|
|
251
|
+
justifyContent: getJustifyContent(),
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
switch (variant) {
|
|
255
|
+
case 'underline':
|
|
256
|
+
return {
|
|
257
|
+
...baseStyle,
|
|
258
|
+
backgroundColor: 'transparent',
|
|
259
|
+
borderBottomWidth: 1,
|
|
260
|
+
borderBottomColor: '#e5e5e5',
|
|
261
|
+
padding: 0,
|
|
262
|
+
};
|
|
263
|
+
case 'boxed':
|
|
264
|
+
return {
|
|
265
|
+
...baseStyle,
|
|
266
|
+
borderRadius: tabBorderRadius,
|
|
267
|
+
borderWidth: 1,
|
|
268
|
+
borderColor: '#e5e5e5',
|
|
269
|
+
padding: 0,
|
|
270
|
+
};
|
|
271
|
+
default: // pills
|
|
272
|
+
return {
|
|
273
|
+
...baseStyle,
|
|
274
|
+
borderRadius: tabListBorderRadius,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const getTabButtonStyle = (isActive: boolean, isDisabled: boolean): ViewStyle => {
|
|
280
|
+
// Phase 2: Determine width based on sizing mode
|
|
281
|
+
let widthStyle: ViewStyle = {};
|
|
282
|
+
if (tabSizing === 'equal' && tabAlignment === 'stretch') {
|
|
283
|
+
widthStyle = { flex: 1 };
|
|
284
|
+
} else if (tabSizing === 'fixed' && fixedTabWidth) {
|
|
285
|
+
widthStyle = { width: fixedTabWidth };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Phase 2: Determine background color
|
|
289
|
+
const bgColor = isDisabled
|
|
290
|
+
? disabledTabFill
|
|
291
|
+
: (isActive ? activeTabColor : inactiveTabColor);
|
|
292
|
+
|
|
293
|
+
const baseStyle: ViewStyle = {
|
|
294
|
+
...widthStyle,
|
|
295
|
+
paddingVertical: tabPaddingY,
|
|
296
|
+
paddingHorizontal: tabPaddingX,
|
|
297
|
+
alignItems: 'center',
|
|
298
|
+
justifyContent: 'center',
|
|
299
|
+
marginHorizontal: tabGap / 2,
|
|
300
|
+
opacity: isDisabled ? 0.5 : 1,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
switch (variant) {
|
|
304
|
+
case 'underline':
|
|
305
|
+
return {
|
|
306
|
+
...baseStyle,
|
|
307
|
+
marginHorizontal: 0,
|
|
308
|
+
borderBottomWidth: isActive ? 2 : 0,
|
|
309
|
+
borderBottomColor: isActive ? activeTextColor : 'transparent',
|
|
310
|
+
};
|
|
311
|
+
case 'boxed':
|
|
312
|
+
return {
|
|
313
|
+
...baseStyle,
|
|
314
|
+
marginHorizontal: 0,
|
|
315
|
+
backgroundColor: bgColor,
|
|
316
|
+
};
|
|
317
|
+
default: // pills
|
|
318
|
+
return {
|
|
319
|
+
...baseStyle,
|
|
320
|
+
backgroundColor: bgColor,
|
|
321
|
+
borderRadius: tabBorderRadius,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const getTabTextStyle = (isActive: boolean, isDisabled: boolean): TextStyle => {
|
|
327
|
+
const textColor = isDisabled
|
|
328
|
+
? disabledTextColor
|
|
329
|
+
: (isActive ? activeTextColor : inactiveTextColor);
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
fontSize: tabFontSize,
|
|
333
|
+
fontWeight: isActive ? (tabFontWeight as TextStyle['fontWeight']) : '500',
|
|
334
|
+
color: textColor,
|
|
335
|
+
};
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// Phase 2: Render tab content with icon
|
|
339
|
+
const renderTabContent = (index: number, isActive: boolean, isDisabled: boolean) => {
|
|
340
|
+
const label = getTabLabel(index);
|
|
341
|
+
const icon = getTabIcon(index);
|
|
342
|
+
const textStyle = getTabTextStyle(isActive, isDisabled);
|
|
343
|
+
|
|
344
|
+
if (!icon || tabIconPosition === 'only') {
|
|
345
|
+
if (icon && tabIconPosition === 'only') {
|
|
346
|
+
return <Text style={[textStyle, { fontSize: tabFontSize + 4 }]}>{icon}</Text>;
|
|
347
|
+
}
|
|
348
|
+
return <Text style={textStyle}>{label}</Text>;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const iconElement = <Text style={[textStyle, { fontSize: tabFontSize + 2, marginHorizontal: 4 }]}>{icon}</Text>;
|
|
352
|
+
const labelElement = <Text style={textStyle}>{label}</Text>;
|
|
353
|
+
|
|
354
|
+
switch (tabIconPosition) {
|
|
355
|
+
case 'right':
|
|
356
|
+
return (
|
|
357
|
+
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
358
|
+
{labelElement}
|
|
359
|
+
{iconElement}
|
|
360
|
+
</View>
|
|
361
|
+
);
|
|
362
|
+
case 'top':
|
|
363
|
+
return (
|
|
364
|
+
<View style={{ alignItems: 'center' }}>
|
|
365
|
+
{iconElement}
|
|
366
|
+
{labelElement}
|
|
367
|
+
</View>
|
|
368
|
+
);
|
|
369
|
+
case 'left':
|
|
370
|
+
default:
|
|
371
|
+
return (
|
|
372
|
+
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
373
|
+
{iconElement}
|
|
374
|
+
{labelElement}
|
|
375
|
+
</View>
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// Render tab list
|
|
381
|
+
const renderTabList = () => {
|
|
382
|
+
const actualTabCount = tabChildren.length || tabCount;
|
|
383
|
+
const tabButtons = Array.from({ length: actualTabCount }, (_, index) => {
|
|
384
|
+
const isActive = index === currentIndex;
|
|
385
|
+
const disabled = isTabDisabled(index);
|
|
386
|
+
|
|
387
|
+
return (
|
|
388
|
+
<React.Fragment key={index}>
|
|
389
|
+
<TouchableOpacity
|
|
390
|
+
style={getTabButtonStyle(isActive, disabled)}
|
|
391
|
+
onPress={() => selectTab(index)}
|
|
392
|
+
activeOpacity={disabled ? 1 : 0.7}
|
|
393
|
+
disabled={disabled}
|
|
394
|
+
>
|
|
395
|
+
{renderTabContent(index, isActive, disabled)}
|
|
396
|
+
</TouchableOpacity>
|
|
397
|
+
{variant === 'boxed' && index < actualTabCount - 1 && (
|
|
398
|
+
<View style={styles.boxedDivider} />
|
|
399
|
+
)}
|
|
400
|
+
</React.Fragment>
|
|
401
|
+
);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const tabListStyle = getTabListStyle();
|
|
405
|
+
|
|
406
|
+
// Phase 2: Handle overflow behavior
|
|
407
|
+
if (overflowBehavior === 'scroll') {
|
|
408
|
+
return (
|
|
409
|
+
<ScrollView
|
|
410
|
+
horizontal
|
|
411
|
+
showsHorizontalScrollIndicator={false}
|
|
412
|
+
contentContainerStyle={tabListStyle}
|
|
413
|
+
>
|
|
414
|
+
{tabButtons}
|
|
415
|
+
</ScrollView>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return (
|
|
420
|
+
<View style={tabListStyle}>
|
|
421
|
+
{tabButtons}
|
|
422
|
+
</View>
|
|
423
|
+
);
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// Render content panel - no fallback to first panel
|
|
427
|
+
const renderContentPanel = () => {
|
|
428
|
+
const panel = contentPanels[currentIndex];
|
|
429
|
+
|
|
430
|
+
if (!panel) {
|
|
431
|
+
// Empty placeholder matching web style
|
|
432
|
+
return (
|
|
433
|
+
<View style={styles.emptyPlaceholder}>
|
|
434
|
+
<Text style={styles.emptyPlaceholderText}>
|
|
435
|
+
No content panel for Tab {currentIndex + 1}
|
|
436
|
+
</Text>
|
|
437
|
+
</View>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return (
|
|
442
|
+
<View style={styles.contentPanel}>
|
|
443
|
+
{renderChild(panel, currentIndex)}
|
|
444
|
+
</View>
|
|
445
|
+
);
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
if (!component.children?.length) {
|
|
449
|
+
return (
|
|
450
|
+
<View style={styles.emptyContainer}>
|
|
451
|
+
<Text style={styles.emptyText}>Add tabs to the component</Text>
|
|
452
|
+
</View>
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
<View style={styles.container}>
|
|
458
|
+
{renderTabList()}
|
|
459
|
+
{renderContentPanel()}
|
|
460
|
+
</View>
|
|
461
|
+
);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const styles = StyleSheet.create({
|
|
465
|
+
container: {
|
|
466
|
+
flex: 1,
|
|
467
|
+
},
|
|
468
|
+
contentPanel: {
|
|
469
|
+
flex: 1,
|
|
470
|
+
},
|
|
471
|
+
boxedDivider: {
|
|
472
|
+
width: 1,
|
|
473
|
+
backgroundColor: '#e5e5e5',
|
|
474
|
+
},
|
|
475
|
+
emptyContainer: {
|
|
476
|
+
padding: 32,
|
|
477
|
+
alignItems: 'center',
|
|
478
|
+
justifyContent: 'center',
|
|
479
|
+
},
|
|
480
|
+
emptyText: {
|
|
481
|
+
color: '#999',
|
|
482
|
+
fontSize: 14,
|
|
483
|
+
},
|
|
484
|
+
emptyPlaceholder: {
|
|
485
|
+
minHeight: 100,
|
|
486
|
+
backgroundColor: 'rgba(128, 128, 128, 0.1)',
|
|
487
|
+
borderRadius: 8,
|
|
488
|
+
padding: 16,
|
|
489
|
+
alignItems: 'center',
|
|
490
|
+
justifyContent: 'center',
|
|
491
|
+
},
|
|
492
|
+
emptyPlaceholderText: {
|
|
493
|
+
color: 'rgba(128, 128, 128, 0.6)',
|
|
494
|
+
fontSize: 14,
|
|
495
|
+
},
|
|
496
|
+
});
|