@developer_tribe/react-builder 1.2.38 → 1.2.40
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/dist/build-components/BIcon/BIconProps.generated.d.ts +1 -1
- package/dist/build-components/CountDown/CountDownProps.generated.d.ts +1 -1
- package/dist/build-components/Image/ImageProps.generated.d.ts +2 -0
- package/dist/build-components/NavigationBarColor/NavigationBarColorProps.generated.d.ts +1 -40
- package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +1 -1
- package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +3 -1
- package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +1 -1
- package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +1 -1
- package/dist/build-components/PaywallBackground/PaywallBackgroundProps.generated.d.ts +1 -1
- package/dist/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.d.ts +1 -1
- package/dist/build-components/PaywallSubscribeButton/PaywallSubscribeButtonProps.generated.d.ts +1 -1
- package/dist/build-components/PriceTag/PriceTagProps.generated.d.ts +1 -1
- package/dist/build-components/Pricing/PricingProps.generated.d.ts +1 -1
- package/dist/build-components/Promo/PromoProps.generated.d.ts +1 -1
- package/dist/build-components/StatusBarColor/StatusBarColorProps.generated.d.ts +1 -1
- package/dist/build-components/patterns.generated.d.ts +51 -356
- package/dist/components/BuilderProvider.d.ts +1 -0
- package/dist/components/DeviceButton.d.ts +4 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.web.cjs.js +4 -4
- package/dist/index.web.cjs.js.map +1 -1
- package/dist/index.web.esm.js +4 -4
- package/dist/index.web.esm.js.map +1 -1
- package/dist/mockOS/context/MockOSContext.d.ts +3 -1
- package/dist/size-matters/index.d.ts +1 -1
- package/dist/store.d.ts +6 -0
- package/dist/styles.css +1 -1
- package/dist/types/Device.d.ts +5 -0
- package/dist/utils/extractTextStyle/extractTextStyle.d.ts +1 -0
- package/dist/utils/extractViewStyle/extractViewStyle.d.ts +1 -0
- package/package.json +1 -1
- package/scripts/prebuild/assets/prompt_scheme.md +7 -0
- package/src/DeviceMockFrame.tsx +8 -0
- package/src/RenderPage.tsx +3 -0
- package/src/assets/devices.json +747 -183
- package/src/assets/meta.json +1 -1
- package/src/assets/prompt-scheme-onboard.generated.ts +1 -1
- package/src/assets/prompt-scheme-paywall.generated.ts +1 -1
- package/src/assets/samples/carousel-sample.json +30 -26
- package/src/assets/samples/paywall-1.json +30 -30
- package/src/assets/samples/paywall-2.json +26 -26
- package/src/assets/samples/paywall-app-delete-offer.json +27 -27
- package/src/assets/samples/paywall-app-open-offer.json +27 -27
- package/src/assets/samples/paywall-back-offer.json +27 -27
- package/src/assets/samples/paywall-notification-offer.json +27 -27
- package/src/assets/samples/simple-1.json +4 -4
- package/src/assets/samples/simple-2.json +25 -25
- package/src/assets/samples/unmigrated-builder-1.1.1.json +7 -7
- package/src/assets/samples/unmigrated-builder1.json +4 -4
- package/src/assets/samples/unvalidated-builder1.json +4 -4
- package/src/assets/samples/unvalidated-crash1.json +2 -2
- package/src/assets/samples/unvalidated-crashcomponent1.json +2 -2
- package/src/assets/samples/vpn-onboard-1.json +34 -30
- package/src/assets/samples/vpn-onboard-2.json +34 -30
- package/src/assets/samples/vpn-onboard-3.json +28 -27
- package/src/assets/samples/vpn-onboard-4.json +28 -27
- package/src/assets/samples/vpn-onboard-5.json +46 -40
- package/src/assets/samples/vpn-onboard-6.json +34 -30
- package/src/assets/samples/vpn-onboard-7.json +33 -29
- package/src/attribute-analyser/style/web/useExtractImageStyle.ts +8 -3
- package/src/attribute-analyser/style/web/useExtractViewStyle.ts +8 -3
- package/src/build-components/BIcon/BIconProps.generated.ts +1 -1
- package/src/build-components/BIcon/pattern.json +1 -2
- package/src/build-components/BackgroundImage/pattern.json +1 -2
- package/src/build-components/Button/pattern.json +1 -2
- package/src/build-components/Carousel/pattern.json +1 -2
- package/src/build-components/CarouselButtons/pattern.json +1 -2
- package/src/build-components/CarouselDots/CarouselDots.tsx +8 -3
- package/src/build-components/CarouselDots/pattern.json +1 -2
- package/src/build-components/CarouselItem/pattern.json +1 -4
- package/src/build-components/CarouselProvider/pattern.json +0 -3
- package/src/build-components/CountDown/CountDownProps.generated.ts +1 -1
- package/src/build-components/CountDown/pattern.json +1 -2
- package/src/build-components/Image/ImageProps.generated.ts +6 -0
- package/src/build-components/Image/pattern.json +10 -2
- package/src/build-components/Main/Main.tsx +3 -1
- package/src/build-components/Main/pattern.json +1 -2
- package/src/build-components/NavigationBarColor/NavigationBarColor.tsx +15 -1
- package/src/build-components/NavigationBarColor/NavigationBarColorProps.generated.ts +1 -52
- package/src/build-components/NavigationBarColor/pattern.json +10 -2
- package/src/build-components/Onboard/pattern.json +1 -4
- package/src/build-components/OnboardButtons/pattern.json +1 -2
- package/src/build-components/OnboardDot/OnboardDot.tsx +3 -2
- package/src/build-components/OnboardDot/pattern.json +1 -2
- package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +1 -1
- package/src/build-components/OnboardFooter/pattern.json +1 -2
- package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +7 -1
- package/src/build-components/OnboardImage/pattern.json +1 -2
- package/src/build-components/OnboardItem/pattern.json +1 -2
- package/src/build-components/OnboardProvider/pattern.json +1 -2
- package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +1 -1
- package/src/build-components/OnboardSubtitle/pattern.json +1 -4
- package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +1 -1
- package/src/build-components/OnboardTitle/pattern.json +1 -4
- package/src/build-components/PaywallBackground/PaywallBackgroundProps.generated.ts +1 -1
- package/src/build-components/PaywallBackground/pattern.json +0 -3
- package/src/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.ts +1 -1
- package/src/build-components/PaywallCloseButton/pattern.json +2 -4
- package/src/build-components/PaywallOptions/pattern.json +1 -4
- package/src/build-components/PaywallProvider/pattern.json +1 -2
- package/src/build-components/PaywallSubscribeButton/PaywallSubscribeButtonProps.generated.ts +1 -1
- package/src/build-components/PaywallSubscribeButton/pattern.json +1 -4
- package/src/build-components/PriceTag/PriceTagProps.generated.ts +1 -1
- package/src/build-components/PriceTag/pattern.json +1 -2
- package/src/build-components/Pricing/PricingProps.generated.ts +1 -1
- package/src/build-components/Pricing/pattern.json +1 -4
- package/src/build-components/Promo/PromoProps.generated.ts +1 -1
- package/src/build-components/Promo/pattern.json +1 -4
- package/src/build-components/RadioButton/pattern.json +1 -2
- package/src/build-components/StatusBarColor/StatusBarColor.tsx +15 -1
- package/src/build-components/StatusBarColor/StatusBarColorProps.generated.ts +1 -1
- package/src/build-components/StatusBarColor/pattern.json +10 -1
- package/src/build-components/Text/pattern.json +1 -2
- package/src/build-components/patterns.generated.ts +57 -376
- package/src/components/BuilderProvider.tsx +1 -0
- package/src/components/DeviceButton.tsx +35 -0
- package/src/components/EditorHeader.tsx +16 -1
- package/src/hooks/useSafeAreaViewStyle.ts +24 -4
- package/src/mockOS/context/MockOSContext.tsx +41 -13
- package/src/modals/DeviceSelectorModal.tsx +94 -10
- package/src/product-base/extractAndroidParams.ts +38 -8
- package/src/size-matters/index.ts +15 -9
- package/src/store.ts +27 -0
- package/src/styles/modals/_product-edit-modal.scss +2 -2
- package/src/types/Device.ts +5 -0
- package/src/utils/analyseNodeByPatterns.ts +6 -2
- package/src/utils/extractTextStyle/extractTextStyle.ts +3 -1
- package/src/utils/extractTextStyle/extractTextStyleNative.ts +1 -1
- package/src/utils/extractViewStyle/extractViewStyle.ts +19 -5
- package/src/utils/extractViewStyle/extractViewStyleNative.ts +5 -1
- package/src/utils/replaceLocalizationParams.ts +5 -7
|
@@ -40,6 +40,8 @@ export function EditorHeader({
|
|
|
40
40
|
setLocalization,
|
|
41
41
|
setBaseSize,
|
|
42
42
|
setProjectColors,
|
|
43
|
+
favoriteDevices,
|
|
44
|
+
toggleFavoriteDevice,
|
|
43
45
|
} = useRenderStore((s) => ({
|
|
44
46
|
device: s.device,
|
|
45
47
|
setDevice: s.setDevice,
|
|
@@ -47,8 +49,19 @@ export function EditorHeader({
|
|
|
47
49
|
setLocalization: s.setLocalization,
|
|
48
50
|
setBaseSize: s.setBaseSize,
|
|
49
51
|
setProjectColors: s.setProjectColors,
|
|
52
|
+
favoriteDevices: s.favoriteDevices || [],
|
|
53
|
+
toggleFavoriteDevice: s.toggleFavoriteDevice,
|
|
50
54
|
}));
|
|
51
55
|
|
|
56
|
+
const headerDevices = useMemo(() => {
|
|
57
|
+
const favs = favoriteDevices
|
|
58
|
+
.map((name) => devices.find((d) => d.name === name))
|
|
59
|
+
.filter((d): d is Device => d !== undefined);
|
|
60
|
+
|
|
61
|
+
const others = devices.filter((d) => !favoriteDevices.includes(d.name));
|
|
62
|
+
return [...favs, ...others].slice(0, 5);
|
|
63
|
+
}, [favoriteDevices]);
|
|
64
|
+
|
|
52
65
|
const sortedSamples = useMemo(() => {
|
|
53
66
|
const weight = (t?: Project['type']) => {
|
|
54
67
|
if (t === 'paywall') return 0;
|
|
@@ -249,12 +262,14 @@ export function EditorHeader({
|
|
|
249
262
|
aria-label="Editor utility header"
|
|
250
263
|
>
|
|
251
264
|
<div className="editor-header__devices">
|
|
252
|
-
{
|
|
265
|
+
{headerDevices.map((deviceOption: Device) => (
|
|
253
266
|
<DeviceButton
|
|
254
267
|
key={deviceOption.name}
|
|
255
268
|
selectedDevice={selectedDevice}
|
|
256
269
|
onSelect={setDevice}
|
|
257
270
|
device={deviceOption}
|
|
271
|
+
isFavorite={favoriteDevices.includes(deviceOption.name)}
|
|
272
|
+
onToggleFavorite={(d) => toggleFavoriteDevice(d.name)}
|
|
258
273
|
/>
|
|
259
274
|
))}
|
|
260
275
|
<button
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import type { Device } from '../types/Device';
|
|
3
|
+
import { useRenderStore } from '../store';
|
|
3
4
|
|
|
4
5
|
function addInset(
|
|
5
6
|
base: React.CSSProperties['paddingTop'],
|
|
@@ -30,17 +31,30 @@ export function useSafeAreaViewStyle(
|
|
|
30
31
|
enabled: boolean,
|
|
31
32
|
device?: Device,
|
|
32
33
|
) {
|
|
34
|
+
const statusBarOverrideTranslucent = useRenderStore(
|
|
35
|
+
(s) => s.statusBarOverrideTranslucent,
|
|
36
|
+
);
|
|
37
|
+
const navBarOverrideTranslucent = useRenderStore(
|
|
38
|
+
(s) => s.navBarOverrideTranslucent,
|
|
39
|
+
);
|
|
40
|
+
|
|
33
41
|
return useMemo(() => {
|
|
34
42
|
if (!enabled) return baseStyle;
|
|
35
43
|
|
|
36
44
|
const [insetTop, insetRight, , insetLeft] = device?.insets ?? [0, 0, 0, 0];
|
|
37
45
|
|
|
38
46
|
// Match DeviceMockFrame fallbacks: status bar overlays content, so we treat it as top safe area.
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
// If translucent is true, we do not apply safe area to top.
|
|
48
|
+
const top = statusBarOverrideTranslucent
|
|
49
|
+
? 0
|
|
50
|
+
: insetTop ||
|
|
51
|
+
(device?.platform === 'ios' ? 20 : device?.platform ? 24 : 0);
|
|
41
52
|
|
|
42
53
|
// Bottom safe area is handled visually by the mock navigation bar area, which takes layout space.
|
|
43
|
-
//
|
|
54
|
+
// However, if we need to support insets for bottom when not rendered by the mock navigation bar,
|
|
55
|
+
// or if the navigation bar is translucent, we apply the inset here. But the mock frame handles it for now.
|
|
56
|
+
// Since Main acts as the safe area provider, if navigation bar is translucent it shouldn't have bottom inset,
|
|
57
|
+
// but right now it is 0. If it were relying on bottom inset, we would check `navBarOverrideTranslucent ? 0 : bottom`.
|
|
44
58
|
const right = insetRight ?? 0;
|
|
45
59
|
const left = insetLeft ?? 0;
|
|
46
60
|
const bottom = 0;
|
|
@@ -60,5 +74,11 @@ export function useSafeAreaViewStyle(
|
|
|
60
74
|
width: subtractInset(baseStyle.width ?? '100%', horizontal),
|
|
61
75
|
height: subtractInset(baseStyle.height ?? '100%', vertical),
|
|
62
76
|
};
|
|
63
|
-
}, [
|
|
77
|
+
}, [
|
|
78
|
+
enabled,
|
|
79
|
+
baseStyle,
|
|
80
|
+
device,
|
|
81
|
+
statusBarOverrideTranslucent,
|
|
82
|
+
navBarOverrideTranslucent,
|
|
83
|
+
]);
|
|
64
84
|
}
|
|
@@ -32,12 +32,14 @@ interface MockOSProviderProps {
|
|
|
32
32
|
statusBarBackgroundColor: string;
|
|
33
33
|
statusBarPlatform: Device['platform'];
|
|
34
34
|
statusBarIsDark: boolean;
|
|
35
|
+
statusBarTranslucent?: boolean;
|
|
35
36
|
// Navigation Bar props
|
|
36
37
|
navBarHeight: number;
|
|
37
38
|
navBarBackgroundColor: string;
|
|
38
39
|
navBarPlatform: Device['platform'];
|
|
39
40
|
navBarNavigationBarType: Device['navigationBarType'];
|
|
40
41
|
navBarIsDark: boolean;
|
|
42
|
+
navBarTranslucent?: boolean;
|
|
41
43
|
// Insets
|
|
42
44
|
insetLeft: number;
|
|
43
45
|
insetRight: number;
|
|
@@ -51,11 +53,13 @@ export function MockOSProvider({
|
|
|
51
53
|
statusBarBackgroundColor,
|
|
52
54
|
statusBarPlatform,
|
|
53
55
|
statusBarIsDark,
|
|
56
|
+
statusBarTranslucent = false,
|
|
54
57
|
navBarHeight,
|
|
55
58
|
navBarBackgroundColor,
|
|
56
59
|
navBarPlatform,
|
|
57
60
|
navBarNavigationBarType,
|
|
58
61
|
navBarIsDark,
|
|
62
|
+
navBarTranslucent = false,
|
|
59
63
|
}: MockOSProviderProps) {
|
|
60
64
|
const [permission, setPermission] = useState<PermissionType | string | null>(
|
|
61
65
|
null,
|
|
@@ -217,31 +221,55 @@ export function MockOSProvider({
|
|
|
217
221
|
onCancel={() => resolveSubscriptionPurchase(false)}
|
|
218
222
|
/>
|
|
219
223
|
)}
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
<div
|
|
225
|
+
className="device-status-bar-container"
|
|
226
|
+
style={{
|
|
227
|
+
position: statusBarTranslucent ? 'absolute' : 'relative',
|
|
228
|
+
top: 0,
|
|
229
|
+
left: 0,
|
|
230
|
+
right: 0,
|
|
231
|
+
zIndex: 10,
|
|
232
|
+
}}
|
|
233
|
+
>
|
|
234
|
+
<DeviceStatusBar
|
|
235
|
+
height={statusBarHeight}
|
|
236
|
+
backgroundColor={statusBarBackgroundColor}
|
|
237
|
+
platform={statusBarPlatform}
|
|
238
|
+
isDark={statusBarIsDark}
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
226
241
|
<div
|
|
227
242
|
className="device-content"
|
|
228
243
|
style={{
|
|
229
244
|
flex: 1,
|
|
230
245
|
overflow: 'hidden',
|
|
231
246
|
position: 'relative',
|
|
247
|
+
// If nav bar is translucent, let the content area stretch under it
|
|
248
|
+
marginBottom: navBarTranslucent ? -navBarHeight : 0,
|
|
232
249
|
}}
|
|
233
250
|
>
|
|
234
251
|
<MockOSRouter childrenBelongTo="app" appName={appName}>
|
|
235
252
|
{children}
|
|
236
253
|
</MockOSRouter>
|
|
237
254
|
</div>
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
255
|
+
<div
|
|
256
|
+
className="device-navigation-bar-container"
|
|
257
|
+
style={{
|
|
258
|
+
position: navBarTranslucent ? 'absolute' : 'relative',
|
|
259
|
+
bottom: 0,
|
|
260
|
+
left: 0,
|
|
261
|
+
right: 0,
|
|
262
|
+
zIndex: 10,
|
|
263
|
+
}}
|
|
264
|
+
>
|
|
265
|
+
<DeviceNavigationBar
|
|
266
|
+
height={navBarHeight}
|
|
267
|
+
backgroundColor={navBarBackgroundColor}
|
|
268
|
+
platform={navBarPlatform}
|
|
269
|
+
navigationBarType={navBarNavigationBarType}
|
|
270
|
+
isDark={navBarIsDark}
|
|
271
|
+
/>
|
|
272
|
+
</div>
|
|
245
273
|
</MockOSContext.Provider>
|
|
246
274
|
);
|
|
247
275
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
2
|
import { Device } from '../types/Device';
|
|
3
3
|
import Modal from './Modal';
|
|
4
4
|
import { DeviceButton } from '../components/DeviceButton';
|
|
5
|
+
import { useRenderStore } from '../store';
|
|
5
6
|
|
|
6
7
|
type DeviceSelectorModalProps = {
|
|
7
8
|
devices: Device[];
|
|
@@ -16,11 +17,36 @@ export function DeviceSelectorModal({
|
|
|
16
17
|
onSelect,
|
|
17
18
|
onClose,
|
|
18
19
|
}: DeviceSelectorModalProps) {
|
|
20
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
21
|
+
const { favoriteDevices, toggleFavoriteDevice } = useRenderStore((state) => ({
|
|
22
|
+
favoriteDevices: state.favoriteDevices || [],
|
|
23
|
+
toggleFavoriteDevice: state.toggleFavoriteDevice,
|
|
24
|
+
}));
|
|
25
|
+
|
|
19
26
|
const handleDeviceSelect = (device: Device) => {
|
|
20
27
|
onSelect(device);
|
|
21
28
|
onClose();
|
|
22
29
|
};
|
|
23
30
|
|
|
31
|
+
const filteredDevices = useMemo(() => {
|
|
32
|
+
return devices.filter((device) =>
|
|
33
|
+
device.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
34
|
+
);
|
|
35
|
+
}, [devices, searchTerm]);
|
|
36
|
+
|
|
37
|
+
const { favorites, others } = useMemo(() => {
|
|
38
|
+
const favs: Device[] = [];
|
|
39
|
+
const rest: Device[] = [];
|
|
40
|
+
filteredDevices.forEach((device) => {
|
|
41
|
+
if (favoriteDevices.includes(device.name)) {
|
|
42
|
+
favs.push(device);
|
|
43
|
+
} else {
|
|
44
|
+
rest.push(device);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return { favorites: favs, others: rest };
|
|
48
|
+
}, [filteredDevices, favoriteDevices]);
|
|
49
|
+
|
|
24
50
|
return (
|
|
25
51
|
<Modal
|
|
26
52
|
onClose={onClose}
|
|
@@ -40,15 +66,73 @@ export function DeviceSelectorModal({
|
|
|
40
66
|
Close
|
|
41
67
|
</button>
|
|
42
68
|
</div>
|
|
43
|
-
<div
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
<div
|
|
70
|
+
className="device-selector-modal__search"
|
|
71
|
+
style={{ padding: '0 16px', marginBottom: '8px' }}
|
|
72
|
+
>
|
|
73
|
+
<input
|
|
74
|
+
type="text"
|
|
75
|
+
placeholder="Search devices..."
|
|
76
|
+
value={searchTerm}
|
|
77
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
78
|
+
className="editor-input"
|
|
79
|
+
style={{ width: '100%', padding: '8px', boxSizing: 'border-box' }}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
<div
|
|
83
|
+
className="device-selector-modal__body"
|
|
84
|
+
style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}
|
|
85
|
+
>
|
|
86
|
+
{favorites.length > 0 && (
|
|
87
|
+
<section>
|
|
88
|
+
<h4
|
|
89
|
+
style={{
|
|
90
|
+
margin: '0 16px 8px',
|
|
91
|
+
fontSize: '14px',
|
|
92
|
+
fontWeight: 'bold',
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
Favorites
|
|
96
|
+
</h4>
|
|
97
|
+
<div className="device-selector-modal__grid" role="list">
|
|
98
|
+
{favorites.map((device) => (
|
|
99
|
+
<DeviceButton
|
|
100
|
+
key={`fav-${device.name}`}
|
|
101
|
+
device={device}
|
|
102
|
+
selectedDevice={selectedDevice}
|
|
103
|
+
onSelect={handleDeviceSelect}
|
|
104
|
+
isFavorite={true}
|
|
105
|
+
onToggleFavorite={(d) => toggleFavoriteDevice(d.name)}
|
|
106
|
+
/>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
</section>
|
|
110
|
+
)}
|
|
111
|
+
<section>
|
|
112
|
+
{favorites.length > 0 && (
|
|
113
|
+
<h4
|
|
114
|
+
style={{
|
|
115
|
+
margin: '0 16px 8px',
|
|
116
|
+
fontSize: '14px',
|
|
117
|
+
fontWeight: 'bold',
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
All Devices
|
|
121
|
+
</h4>
|
|
122
|
+
)}
|
|
123
|
+
<div className="device-selector-modal__grid" role="list">
|
|
124
|
+
{others.map((device) => (
|
|
125
|
+
<DeviceButton
|
|
126
|
+
key={`other-${device.name}`}
|
|
127
|
+
device={device}
|
|
128
|
+
selectedDevice={selectedDevice}
|
|
129
|
+
onSelect={handleDeviceSelect}
|
|
130
|
+
isFavorite={false}
|
|
131
|
+
onToggleFavorite={(d) => toggleFavoriteDevice(d.name)}
|
|
132
|
+
/>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
</section>
|
|
52
136
|
</div>
|
|
53
137
|
</Modal>
|
|
54
138
|
);
|
|
@@ -70,7 +70,7 @@ export function extractAndroidParams(
|
|
|
70
70
|
iapLogger.warn(['extractAndroidParams'], 'No offers found in product', {
|
|
71
71
|
productId: product.id || product.productId,
|
|
72
72
|
});
|
|
73
|
-
return
|
|
73
|
+
return getFallbackParams(product);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
const pricingPhases =
|
|
@@ -81,7 +81,7 @@ export function extractAndroidParams(
|
|
|
81
81
|
productId: product.id || product.productId,
|
|
82
82
|
offerId: selectedOffer.id,
|
|
83
83
|
});
|
|
84
|
-
return
|
|
84
|
+
return getFallbackParams(product);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// Trial phase: priceAmountMicros === "0"
|
|
@@ -105,7 +105,7 @@ export function extractAndroidParams(
|
|
|
105
105
|
productId: product.id || product.productId,
|
|
106
106
|
pricingPhasesCount: pricingPhases.length,
|
|
107
107
|
});
|
|
108
|
-
return
|
|
108
|
+
return getFallbackParams(product);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
const regularPeriod = parseBillingPeriod(regularPhase.billingPeriod);
|
|
@@ -142,15 +142,31 @@ export function extractAndroidParams(
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
const discountPercentage = calculateDiscount(price, promoPrice);
|
|
145
|
-
|
|
145
|
+
|
|
146
|
+
// Apply root properties if they exist (allows mock edits to immediately bypass deep structures)
|
|
147
|
+
const finalPrice =
|
|
148
|
+
typeof product.price === 'string' && product.price !== ''
|
|
149
|
+
? product.price
|
|
150
|
+
: price;
|
|
151
|
+
const finalCurrency =
|
|
152
|
+
typeof product.currency === 'string' && product.currency !== ''
|
|
153
|
+
? product.currency
|
|
154
|
+
: currency;
|
|
155
|
+
const finalLocalizedPrice =
|
|
156
|
+
typeof product.localizedPrice === 'string' &&
|
|
157
|
+
product.localizedPrice !== ''
|
|
158
|
+
? product.localizedPrice
|
|
159
|
+
: localizedPrice;
|
|
160
|
+
|
|
161
|
+
const priceNum = parseFloat(finalPrice);
|
|
146
162
|
const pricePerMonth = calculatePricePerMonth(priceNum, regularPeriod.unit);
|
|
147
163
|
const pricePerYear = calculatePricePerYear(priceNum, regularPeriod.unit);
|
|
148
164
|
|
|
149
165
|
return {
|
|
150
|
-
price,
|
|
166
|
+
price: finalPrice,
|
|
151
167
|
promoPrice,
|
|
152
|
-
currency,
|
|
153
|
-
localizedPrice,
|
|
168
|
+
currency: finalCurrency,
|
|
169
|
+
localizedPrice: finalLocalizedPrice,
|
|
154
170
|
period: regularPeriod.unit,
|
|
155
171
|
periodValue: String(regularPeriod.value),
|
|
156
172
|
periodType,
|
|
@@ -173,10 +189,24 @@ export function extractAndroidParams(
|
|
|
173
189
|
error: error instanceof Error ? error.message : String(error),
|
|
174
190
|
},
|
|
175
191
|
);
|
|
176
|
-
return
|
|
192
|
+
return getFallbackParams(product);
|
|
177
193
|
}
|
|
178
194
|
}
|
|
179
195
|
|
|
196
|
+
/** Boş params ama mock product price'ları ile birleştirilmiş (fallback) */
|
|
197
|
+
function getFallbackParams(product: Product): AndroidParams {
|
|
198
|
+
const price = String(product.price || product.localizedPrice || '').replace(
|
|
199
|
+
/[^0-9.]/g,
|
|
200
|
+
'',
|
|
201
|
+
);
|
|
202
|
+
return {
|
|
203
|
+
...getEmptyParams(),
|
|
204
|
+
price,
|
|
205
|
+
currency: product.currency || product.currencyCode || '',
|
|
206
|
+
localizedPrice: product.localizedPrice || '',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
180
210
|
/** Boş params döner (fallback) */
|
|
181
211
|
function getEmptyParams(): AndroidParams {
|
|
182
212
|
return {
|
|
@@ -52,15 +52,21 @@ export const ms = (size: number, factor?: number) =>
|
|
|
52
52
|
|
|
53
53
|
export function parseSize(
|
|
54
54
|
value: string | number | undefined,
|
|
55
|
-
baseSize
|
|
56
|
-
device
|
|
55
|
+
baseSize: BaseSize | undefined,
|
|
56
|
+
device: Device | 'native' | undefined,
|
|
57
57
|
) {
|
|
58
58
|
if (value === undefined) return undefined;
|
|
59
|
+
|
|
60
|
+
const multiplier = device === 'native' ? 1 : (device?.multiplier ?? 1);
|
|
61
|
+
|
|
59
62
|
if (typeof value === 'number') {
|
|
60
|
-
return value;
|
|
63
|
+
return value * multiplier;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
const scalers = getScalers(
|
|
66
|
+
const scalers = getScalers(
|
|
67
|
+
baseSize,
|
|
68
|
+
device === 'native' ? undefined : device,
|
|
69
|
+
);
|
|
64
70
|
|
|
65
71
|
const raw = String(value).trim();
|
|
66
72
|
const lower = raw.toLowerCase();
|
|
@@ -68,16 +74,16 @@ export function parseSize(
|
|
|
68
74
|
// Handle explicit scalers via suffixes
|
|
69
75
|
if (lower.endsWith('@s')) {
|
|
70
76
|
const n = parseFloat(lower.slice(0, -2));
|
|
71
|
-
return Number.isFinite(n) ? scalers.scale(n) : raw;
|
|
77
|
+
return Number.isFinite(n) ? scalers.scale(n) * multiplier : raw;
|
|
72
78
|
}
|
|
73
79
|
if (lower.endsWith('@vs')) {
|
|
74
80
|
const n = parseFloat(lower.slice(0, -3));
|
|
75
|
-
return Number.isFinite(n) ? scalers.verticalScale(n) : raw;
|
|
81
|
+
return Number.isFinite(n) ? scalers.verticalScale(n) * multiplier : raw;
|
|
76
82
|
}
|
|
77
83
|
if (lower.endsWith('@f') || lower.endsWith('@fs')) {
|
|
78
84
|
const cut = lower.endsWith('@f') ? -2 : -3;
|
|
79
85
|
const n = parseFloat(lower.slice(0, cut));
|
|
80
|
-
return Number.isFinite(n) ? scalers.verticalScale(n) : raw;
|
|
86
|
+
return Number.isFinite(n) ? scalers.verticalScale(n) * multiplier : raw;
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
// Preserve percentage values as-is
|
|
@@ -89,13 +95,13 @@ export function parseSize(
|
|
|
89
95
|
// Handle px explicitly (treat as absolute, not scaled)
|
|
90
96
|
if (lower.endsWith('px')) {
|
|
91
97
|
const n = parseFloat(lower.replace('px', ''));
|
|
92
|
-
return Number.isFinite(n) ? n : raw;
|
|
98
|
+
return Number.isFinite(n) ? n * multiplier : raw;
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
// Plain numeric strings fall back to provided scaler (implicit number)
|
|
96
102
|
const numeric = parseFloat(lower);
|
|
97
103
|
if (Number.isFinite(numeric)) {
|
|
98
|
-
return numeric;
|
|
104
|
+
return numeric * multiplier;
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
// Unknown format, return as-is
|
package/src/store.ts
CHANGED
|
@@ -92,8 +92,15 @@ type RenderStore = {
|
|
|
92
92
|
// OS bar color overrides (set by StatusBarColor / NavigationBarColor build components)
|
|
93
93
|
statusBarOverrideColor: string | null;
|
|
94
94
|
setStatusBarOverrideColor: (color: string | null) => void;
|
|
95
|
+
statusBarOverrideTranslucent: boolean | null;
|
|
96
|
+
setStatusBarOverrideTranslucent: (translucent: boolean | null) => void;
|
|
95
97
|
navBarOverrideColor: string | null;
|
|
96
98
|
setNavBarOverrideColor: (color: string | null) => void;
|
|
99
|
+
navBarOverrideTranslucent: boolean | null;
|
|
100
|
+
setNavBarOverrideTranslucent: (translucent: boolean | null) => void;
|
|
101
|
+
|
|
102
|
+
favoriteDevices: string[];
|
|
103
|
+
toggleFavoriteDevice: (name: string) => void;
|
|
97
104
|
};
|
|
98
105
|
|
|
99
106
|
export const useRenderStore = createWithEqualityFn<RenderStore>()(
|
|
@@ -269,8 +276,26 @@ export const useRenderStore = createWithEqualityFn<RenderStore>()(
|
|
|
269
276
|
statusBarOverrideColor: null,
|
|
270
277
|
setStatusBarOverrideColor: (color) =>
|
|
271
278
|
set({ statusBarOverrideColor: color }),
|
|
279
|
+
statusBarOverrideTranslucent: null,
|
|
280
|
+
setStatusBarOverrideTranslucent: (translucent) =>
|
|
281
|
+
set({ statusBarOverrideTranslucent: translucent }),
|
|
272
282
|
navBarOverrideColor: null,
|
|
273
283
|
setNavBarOverrideColor: (color) => set({ navBarOverrideColor: color }),
|
|
284
|
+
navBarOverrideTranslucent: null,
|
|
285
|
+
setNavBarOverrideTranslucent: (translucent) =>
|
|
286
|
+
set({ navBarOverrideTranslucent: translucent }),
|
|
287
|
+
|
|
288
|
+
favoriteDevices: [],
|
|
289
|
+
toggleFavoriteDevice: (name) =>
|
|
290
|
+
set((state) => {
|
|
291
|
+
const devs = state.favoriteDevices || [];
|
|
292
|
+
const isFav = devs.includes(name);
|
|
293
|
+
return {
|
|
294
|
+
favoriteDevices: isFav
|
|
295
|
+
? devs.filter((n) => n !== name)
|
|
296
|
+
: [...devs, name],
|
|
297
|
+
};
|
|
298
|
+
}),
|
|
274
299
|
}),
|
|
275
300
|
{
|
|
276
301
|
name: 'render-store',
|
|
@@ -281,6 +306,8 @@ export const useRenderStore = createWithEqualityFn<RenderStore>()(
|
|
|
281
306
|
products: state.products,
|
|
282
307
|
benefits: state.benefits,
|
|
283
308
|
listMaxNested: state.listMaxNested,
|
|
309
|
+
favoriteDevices: state.favoriteDevices,
|
|
310
|
+
device: state.device,
|
|
284
311
|
}),
|
|
285
312
|
storage: createJSONStorage(() => localStorage),
|
|
286
313
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
@use '../foundation/sizes' as sizes;
|
|
2
2
|
|
|
3
3
|
.product-edit-modal {
|
|
4
|
-
width: min(
|
|
4
|
+
width: min(800px, calc(100vw - 32px));
|
|
5
5
|
max-height: 90vh;
|
|
6
6
|
padding: sizes.$spaceRoomy;
|
|
7
7
|
display: flex;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
.product-edit-modal__textarea {
|
|
20
|
-
min-height:
|
|
20
|
+
min-height: 140px;
|
|
21
21
|
padding: 8px;
|
|
22
22
|
resize: vertical;
|
|
23
23
|
}
|
package/src/types/Device.ts
CHANGED
|
@@ -35,4 +35,9 @@ export interface Device {
|
|
|
35
35
|
* 1 = highest importance, 100 = lowest importance
|
|
36
36
|
*/
|
|
37
37
|
importance?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Optional multiplier to adjust for differences in CSS viewport sizing between web and native.
|
|
40
|
+
* Example: 0.5 for a device where web renders it twice as large as expected compared to native.
|
|
41
|
+
*/
|
|
42
|
+
multiplier?: number;
|
|
38
43
|
}
|
|
@@ -429,11 +429,15 @@ function validateAttributesByPattern(
|
|
|
429
429
|
if (attrName === 'style' || attrName === 'styles') continue;
|
|
430
430
|
const attrSpec = schema?.[attrName] as AttributeTypeSpec | undefined;
|
|
431
431
|
if (!attrSpec) {
|
|
432
|
-
if (
|
|
432
|
+
if (
|
|
433
|
+
attrName === 'title' ||
|
|
434
|
+
attrName === 'description' ||
|
|
435
|
+
attrName === 'testID'
|
|
436
|
+
) {
|
|
433
437
|
const res = validateAttributeValue(
|
|
434
438
|
pattern.pattern.type,
|
|
435
439
|
attrValue,
|
|
436
|
-
attrName,
|
|
440
|
+
attrName === 'testID' ? 'string' : attrName,
|
|
437
441
|
joinPath(path, attrName),
|
|
438
442
|
);
|
|
439
443
|
if (!res.valid) return res;
|
|
@@ -95,6 +95,7 @@ export type ExtractTextStyleOptions = {
|
|
|
95
95
|
onError?: (error: string) => void;
|
|
96
96
|
directlyTextStyle?: boolean;
|
|
97
97
|
baseSize?: BaseSize;
|
|
98
|
+
device?: import('../../types/Device').Device;
|
|
98
99
|
};
|
|
99
100
|
|
|
100
101
|
export function extractTextStyle<T extends TextPropsGenerated['attributes']>(
|
|
@@ -127,7 +128,7 @@ export function extractTextStyle<T extends TextPropsGenerated['attributes']>(
|
|
|
127
128
|
// Typography
|
|
128
129
|
const fontSize = get('fontSize') as string | number | undefined;
|
|
129
130
|
if (fontSize !== undefined) {
|
|
130
|
-
const parsed = parseSize(fontSize, options.baseSize);
|
|
131
|
+
const parsed = parseSize(fontSize, options.baseSize, options.device!);
|
|
131
132
|
style.fontSize = parsed as React.CSSProperties['fontSize'];
|
|
132
133
|
} else {
|
|
133
134
|
style.fontSize = fs(14);
|
|
@@ -194,6 +195,7 @@ export function extractTextStyle<T extends TextPropsGenerated['attributes']>(
|
|
|
194
195
|
projectColors: options.projectColors,
|
|
195
196
|
theme,
|
|
196
197
|
baseSize: options.baseSize,
|
|
198
|
+
device: options.device,
|
|
197
199
|
});
|
|
198
200
|
const fullStyle = { ...viewStyle, ...style };
|
|
199
201
|
|
|
@@ -63,7 +63,7 @@ export function extractTextStyleNative<
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
const rawFontSize = get('fontSize') as string | number | undefined;
|
|
66
|
-
const parsedFontSize = parseSize(rawFontSize, options.baseSize);
|
|
66
|
+
const parsedFontSize = parseSize(rawFontSize, options.baseSize, 'native');
|
|
67
67
|
if (typeof parsedFontSize === 'number') style.fontSize = parsedFontSize;
|
|
68
68
|
else style.fontSize = fs(14);
|
|
69
69
|
|
|
@@ -8,11 +8,13 @@ import type { BaseSize } from '../../types/PreviewConfig';
|
|
|
8
8
|
import { parseSize } from '../../size-matters';
|
|
9
9
|
import { parseColor } from '../parseColor';
|
|
10
10
|
import { getStyleBag, toAttributeRecord } from '../attributeStyle';
|
|
11
|
+
import { getDefaultDevice } from '../getDevices';
|
|
11
12
|
|
|
12
13
|
export type ExtractViewStyleOptions = {
|
|
13
14
|
projectColors?: ProjectColors;
|
|
14
15
|
theme?: string;
|
|
15
16
|
baseSize?: BaseSize;
|
|
17
|
+
device?: import('../../types/Device').Device;
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
export function extractViewStyle<T extends ViewPropsGenerated['attributes']>(
|
|
@@ -74,7 +76,7 @@ export function extractViewStyle<T extends ViewPropsGenerated['attributes']>(
|
|
|
74
76
|
rawValue: string | number | undefined,
|
|
75
77
|
) => {
|
|
76
78
|
if (isEmptySizeValue(rawValue)) return;
|
|
77
|
-
const parsed = parseSize(rawValue, options.baseSize);
|
|
79
|
+
const parsed = parseSize(rawValue, options.baseSize, options.device!);
|
|
78
80
|
style[property] = parsed as React.CSSProperties[K];
|
|
79
81
|
};
|
|
80
82
|
|
|
@@ -87,13 +89,21 @@ export function extractViewStyle<T extends ViewPropsGenerated['attributes']>(
|
|
|
87
89
|
| number
|
|
88
90
|
| undefined;
|
|
89
91
|
if (!isEmptySizeValue(paddingHorizontal)) {
|
|
90
|
-
const parsed = parseSize(
|
|
92
|
+
const parsed = parseSize(
|
|
93
|
+
paddingHorizontal,
|
|
94
|
+
options.baseSize,
|
|
95
|
+
options.device!,
|
|
96
|
+
);
|
|
91
97
|
style.paddingLeft = parsed as React.CSSProperties['paddingLeft'];
|
|
92
98
|
style.paddingRight = parsed as React.CSSProperties['paddingRight'];
|
|
93
99
|
}
|
|
94
100
|
const paddingVertical = get('paddingVertical') as string | number | undefined;
|
|
95
101
|
if (!isEmptySizeValue(paddingVertical)) {
|
|
96
|
-
const parsed = parseSize(
|
|
102
|
+
const parsed = parseSize(
|
|
103
|
+
paddingVertical,
|
|
104
|
+
options.baseSize,
|
|
105
|
+
options.device!,
|
|
106
|
+
);
|
|
97
107
|
style.paddingTop = parsed as React.CSSProperties['paddingTop'];
|
|
98
108
|
style.paddingBottom = parsed as React.CSSProperties['paddingBottom'];
|
|
99
109
|
}
|
|
@@ -117,14 +127,18 @@ export function extractViewStyle<T extends ViewPropsGenerated['attributes']>(
|
|
|
117
127
|
(attrRecord.marginHorizontal as string | number | undefined) ??
|
|
118
128
|
(styleBag?.marginHorizontal as string | number | undefined);
|
|
119
129
|
if (!isEmptySizeValue(marginHorizontalRaw)) {
|
|
120
|
-
const parsed = parseSize(
|
|
130
|
+
const parsed = parseSize(
|
|
131
|
+
marginHorizontalRaw,
|
|
132
|
+
options.baseSize,
|
|
133
|
+
options.device!,
|
|
134
|
+
);
|
|
121
135
|
style.marginLeft = parsed as React.CSSProperties['marginLeft'];
|
|
122
136
|
style.marginRight = parsed as React.CSSProperties['marginRight'];
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
const marginVertical = get('marginVertical') as string | number | undefined;
|
|
126
140
|
if (!isEmptySizeValue(marginVertical)) {
|
|
127
|
-
const parsed = parseSize(marginVertical, options.baseSize);
|
|
141
|
+
const parsed = parseSize(marginVertical, options.baseSize, options.device!);
|
|
128
142
|
style.marginTop = parsed as React.CSSProperties['marginTop'];
|
|
129
143
|
style.marginBottom = parsed as React.CSSProperties['marginBottom'];
|
|
130
144
|
}
|