@momo-kits/foundation 0.169.9 → 0.169.10-beta.1

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 (56) hide show
  1. package/Application/BottomSheet.hooks.ts +17 -27
  2. package/Application/BottomSheet.tsx +27 -35
  3. package/Application/BottomTab/BottomTabBar.tsx +6 -6
  4. package/Application/BottomTab/DuoRailTabBar.tsx +5 -5
  5. package/Application/BottomTab/TabScreen.tsx +10 -11
  6. package/Application/BottomTab/index.tsx +5 -5
  7. package/Application/BottomTab/usePressLift.ts +3 -3
  8. package/Application/Components/HeaderAnimated.tsx +38 -13
  9. package/Application/Components/HeaderBackground.tsx +16 -3
  10. package/Application/Components/HeaderExtendHeader.tsx +41 -28
  11. package/Application/Components/HeaderLeft.tsx +4 -18
  12. package/Application/Components/HeaderRight.styles.ts +15 -13
  13. package/Application/Components/HeaderTitle.shared.ts +10 -11
  14. package/Application/Components/HeaderToolkitAction.tsx +21 -54
  15. package/Application/Components/NavigationButton.tsx +9 -6
  16. package/Application/Components/SearchHeader.tsx +20 -7
  17. package/Application/Components/useToolkitActions.tsx +21 -22
  18. package/Application/ModalScreen.tsx +15 -19
  19. package/Application/StackScreen.tsx +18 -13
  20. package/Application/headerLayout.ts +165 -0
  21. package/Application/index.ts +11 -1
  22. package/Application/modalLayout.ts +0 -1
  23. package/Application/navigationOptions.tsx +6 -15
  24. package/Application/types.header.ts +7 -2
  25. package/Application/types.ts +7 -0
  26. package/Context/index.ts +1 -3
  27. package/Image/index.tsx +6 -6
  28. package/Input/Input.tsx +37 -40
  29. package/Input/InputDropDown.tsx +10 -13
  30. package/Input/InputMoney.tsx +17 -17
  31. package/Input/InputMoneyView.tsx +12 -12
  32. package/Input/InputOTP.tsx +34 -46
  33. package/Input/InputPhoneNumber.tsx +43 -48
  34. package/Input/InputSearch.tsx +39 -26
  35. package/Input/InputTextArea.tsx +23 -29
  36. package/Input/common.tsx +257 -0
  37. package/Input/index.tsx +7 -1
  38. package/Input/moneyUtils.ts +23 -0
  39. package/Input/utils.ts +7 -126
  40. package/Layout/FloatingButton.tsx +24 -38
  41. package/Layout/Screen.tsx +3 -0
  42. package/Layout/layoutMetrics.ts +34 -43
  43. package/Layout/screenHeader.tsx +55 -52
  44. package/Layout/screenTypes.ts +13 -4
  45. package/Pagination/PaginationScroll.tsx +3 -5
  46. package/Pagination/types.ts +5 -3
  47. package/Popup/PopupNotify.tsx +14 -17
  48. package/Popup/PopupPromotion.tsx +4 -5
  49. package/Popup/types.ts +4 -2
  50. package/Popup/utils.ts +4 -7
  51. package/Skeleton/index.tsx +12 -15
  52. package/package.json +1 -1
  53. package/Input/ErrorView.tsx +0 -59
  54. package/Input/FloatingView.tsx +0 -101
  55. package/Input/RenderTrailing.tsx +0 -79
  56. package/Input/constants.ts +0 -8
@@ -1,66 +1,57 @@
1
- /** Header geometry. */
1
+ /**
2
+ * Header geometry
3
+ * - barHeight: navigation bar row below the status bar
4
+ * - iPhoneMinHeight: header never shorter than this on iPhone (landscape, iPhone Duo)
5
+ * - statusBarMinInset: a smaller top inset means the status bar is not on the top edge
6
+ * - largeWidth / largeMargin: @react-navigation/elements Header gives its content row
7
+ * 5pt side margins on iOS from 414pt wide
8
+ */
2
9
  export const HeaderMetrics = {
3
- /** Navigation bar row below the status bar. */
4
10
  barHeight: 52,
5
- /** Header never shorter than this on iPhone (landscape, iPhone Duo). */
6
11
  iPhoneMinHeight: 72,
7
- /** A smaller top inset means the status bar is not on the top edge. */
8
12
  statusBarMinInset: 20,
9
- /**
10
- * From this width @react-navigation/elements Header gives its content row
11
- * `largeMargin` (5pt) side margins on iOS.
12
- */
13
13
  largeWidth: 414,
14
14
  largeMargin: 5,
15
15
  };
16
16
 
17
- /** iPhone Duo geometry, iOS 27.1 (Xcode 27.1 simulator). */
17
+ /**
18
+ * iPhone Duo geometry, iOS 27.1 (Xcode 27.1 simulator)
19
+ * - maxAspectRatio: both displays are near 1.42 (inner) and 1.45 (outer), every slab
20
+ * iPhone is 1.77 or taller
21
+ * - innerMinHeight: window height from which the inner display is in use (outer display
22
+ * is 466pt tall in landscape, inner display 669pt)
23
+ * - controlSize / itemPitch: vertical bar of 48pt circular controls, item groups as 48pt
24
+ * wide capsules at a 50pt pitch, 12pt from the strip's inner edge and 12pt apart
25
+ * - firstItemTop: with the status bar in the strip, the first control sits under the
26
+ * camera, clock and connectivity cluster (cluster ends near y 151 on the outer display,
27
+ * y 98 on the inner display in landscape)
28
+ * - outerHeaderHeight / outerCameraCenterY: on the outer display in portrait the header
29
+ * is 4pt taller than iPhoneMinHeight and its title lines up with the camera punchout
30
+ * (y 24–66 at the top of the strip)
31
+ * - edgeInset: on the outer display in landscape the strip holds no status bar, so its
32
+ * controls start this far from the top and stay this far above the bottom, unless a
33
+ * reserved region (the camera at either end) asks for more
34
+ * - innerStatusBarCenterY / innerStatusBarWidth: on the inner display in portrait the
35
+ * clock and connectivity cluster sits at the top right corner (x 550–643 of 669, clock
36
+ * digits centered on y 47); its reserved region, when reported, is taller than the glyphs
37
+ * - floatingTabBarHeight / floatingTabBarMargin / floatingTabBarInsetOverlap: the host
38
+ * app's iOS 26 system tab bar on the inner display in portrait, a 62pt Liquid Glass pill
39
+ * sized to its tabs with the QR as a pinned 62pt platter beside it, centered and at
40
+ * least 20pt from the side edges; it overlaps the home indicator inset by 13pt (21pt
41
+ * above the screen bottom with a 34pt inset)
42
+ */
18
43
  export const DuoMetrics = {
19
- /**
20
- * Both displays are near 1.42 (inner) and 1.45 (outer); every slab iPhone is
21
- * 1.77 or taller.
22
- */
23
44
  maxAspectRatio: 1.6,
24
- /**
25
- * Window height from which the inner display is in use (outer display is
26
- * 466pt tall in landscape, inner display 669pt).
27
- */
28
45
  innerMinHeight: 600,
29
- /**
30
- * Vertical bar of 48pt circular controls; item groups are 48pt wide capsules
31
- * at a 50pt pitch, 12pt from the strip's inner edge and 12pt apart.
32
- */
33
46
  controlSize: 48,
34
47
  itemPitch: 50,
35
- /**
36
- * With the status bar in the strip, the first control sits under the camera,
37
- * clock and connectivity cluster (ends near y 151 outer, y 98 inner
38
- * landscape).
39
- */
40
48
  firstItemTop: 170,
41
49
  firstItemTopLandscape: 120,
42
- /**
43
- * Outer display in portrait: the header is 4pt taller than iPhoneMinHeight
44
- * and its title lines up with the camera punchout (y 24–66 at the strip's
45
- * top).
46
- */
47
50
  outerHeaderHeight: 76,
48
51
  outerCameraCenterY: 45,
49
- /**
50
- * Outer display in landscape: no status bar in the strip, so controls keep
51
- * this gap from both ends unless a reserved region (the camera) needs more.
52
- */
53
52
  edgeInset: 24,
54
- /**
55
- * Inner display in portrait: clock and connectivity cluster at the top right
56
- * (x 550–643 of 669, digits on y 47); a reported reserved region is taller.
57
- */
58
53
  innerStatusBarCenterY: 47,
59
54
  innerStatusBarWidth: 120,
60
- /**
61
- * Host app's iOS 26 tab bar, inner display in portrait: a 62pt Liquid Glass
62
- * pill plus 62pt QR platter, ≥ 20pt from the sides, 13pt over the home inset.
63
- */
64
55
  floatingTabBarHeight: 62,
65
56
  floatingTabBarMargin: 20,
66
57
  floatingTabBarInsetOverlap: 13,
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useRef } from 'react';
2
2
  import { StatusBar, StatusBarStyle } from 'react-native';
3
3
  import { StackNavigationOptions } from '@react-navigation/stack';
4
+ import type { SharedValue } from 'react-native-reanimated';
4
5
  import Navigation from '../Application/Navigation';
5
6
  import { AnimatedHeader, SearchHeaderProps } from '../Application/types';
6
7
  import { exportHeaderTitle } from '../Application/navigationOptions';
@@ -12,6 +13,11 @@ import {
12
13
  SearchHeader,
13
14
  } from '../Application';
14
15
  import { DuoGlassBackground } from '../Application/Components/DuoGlassBackground';
16
+ import {
17
+ type HeaderContentLayout,
18
+ useHeaderFrameOptions,
19
+ useHeaderLayout,
20
+ } from '../Application/headerLayout';
15
21
  import { getHeaderMargin } from './utils';
16
22
  import { ANIMATED_BANNER_HEIGHT } from './screenTypes';
17
23
  import { HeaderType } from './types';
@@ -29,6 +35,31 @@ type ScreenHeaderParams = {
29
35
  gradientColor?: string;
30
36
  headerBackground?: string;
31
37
  layoutOffset: number;
38
+ contentLayout?: HeaderContentLayout;
39
+ };
40
+
41
+ /**
42
+ * Transparent header for an `inputSearchProps` Screen: its title fades out over the
43
+ * first 50pt of scroll while the search collapses into the bar.
44
+ */
45
+ const getSearchTitleOptions = (
46
+ sharedValue: SharedValue<number>,
47
+ ): StackNavigationOptions => {
48
+ return {
49
+ headerShown: true,
50
+ headerTransparent: true,
51
+ headerBackground: () => null,
52
+ headerTitle: (props: any) => (
53
+ <HeaderTitle
54
+ {...props}
55
+ interpolate={{
56
+ inputRange: [0, 50],
57
+ outputRange: [1, 0],
58
+ }}
59
+ animatedValue={sharedValue}
60
+ />
61
+ ),
62
+ };
32
63
  };
33
64
 
34
65
  /**
@@ -48,9 +79,12 @@ export const useScreenHeader = ({
48
79
  gradientColor,
49
80
  headerBackground,
50
81
  layoutOffset,
82
+ contentLayout,
51
83
  }: ScreenHeaderParams) => {
52
84
  const currentTint = useRef<string | undefined>(undefined);
53
85
  const appliedHeaderMode = useRef<string | undefined>(undefined);
86
+ const headerLayout = useHeaderLayout(contentLayout);
87
+ const frameOptions = useHeaderFrameOptions(contentLayout);
54
88
 
55
89
  // keepTitle leaves a title the screen set itself when the same type is re-applied
56
90
  const setHeaderType = useCallback(
@@ -79,21 +113,7 @@ export const useScreenHeader = ({
79
113
  headerTitle: (props: any) => <HeaderTitle {...props} />,
80
114
  };
81
115
  if (inputSearchProps) {
82
- options = {
83
- headerShown: true,
84
- headerTransparent: true,
85
- headerBackground: () => null,
86
- headerTitle: (props: any) => (
87
- <HeaderTitle
88
- {...props}
89
- interpolate={{
90
- inputRange: [0, 50],
91
- outputRange: [1, 0],
92
- }}
93
- animatedValue={sharedValue}
94
- />
95
- ),
96
- };
116
+ options = getSearchTitleOptions(sharedValue);
97
117
  }
98
118
  break;
99
119
 
@@ -127,21 +147,7 @@ export const useScreenHeader = ({
127
147
  headerTitle: (props: any) => <HeaderTitle {...props} />,
128
148
  };
129
149
  if (inputSearchProps) {
130
- options = {
131
- headerShown: true,
132
- headerTransparent: true,
133
- headerBackground: () => null,
134
- headerTitle: (props: any) => (
135
- <HeaderTitle
136
- {...props}
137
- interpolate={{
138
- inputRange: [0, 50],
139
- outputRange: [1, 0],
140
- }}
141
- animatedValue={sharedValue}
142
- />
143
- ),
144
- };
150
+ options = getSearchTitleOptions(sharedValue);
145
151
  }
146
152
  }
147
153
 
@@ -149,7 +155,11 @@ export const useScreenHeader = ({
149
155
  delete options.headerTitle;
150
156
  }
151
157
 
152
- navigation?.instance?.setOptions({ header: undefined, ...options });
158
+ navigation?.instance?.setOptions({
159
+ header: undefined,
160
+ ...frameOptions,
161
+ ...options,
162
+ });
153
163
  },
154
164
  [
155
165
  gradientColor,
@@ -160,6 +170,7 @@ export const useScreenHeader = ({
160
170
  useShadowHeader,
161
171
  hasSideBar,
162
172
  quickReturnOptions,
173
+ frameOptions,
163
174
  ],
164
175
  );
165
176
 
@@ -207,7 +218,7 @@ export const useScreenHeader = ({
207
218
  };
208
219
  }
209
220
 
210
- navigation?.instance?.setOptions(options);
221
+ navigation?.instance?.setOptions({ ...frameOptions, ...options });
211
222
  },
212
223
  [
213
224
  gradientColor,
@@ -218,32 +229,23 @@ export const useScreenHeader = ({
218
229
  hasSideBar,
219
230
  layoutOffset,
220
231
  quickReturnOptions,
232
+ frameOptions,
221
233
  ],
222
234
  );
223
235
 
224
236
  const setAnimatedSearch = useCallback(() => {
225
- const options: StackNavigationOptions = {
226
- headerShown: true,
227
- headerTransparent: true,
228
- headerBackground: () => null,
229
- headerTitle: (props: any) => (
230
- <HeaderTitle
231
- {...props}
232
- interpolate={{
233
- inputRange: [0, 50],
234
- outputRange: [1, 0],
235
- }}
236
- animatedValue={sharedValue}
237
- />
238
- ),
239
- };
240
-
241
- navigation?.instance?.setOptions({ header: undefined, ...options });
242
- }, [navigation?.instance, sharedValue]);
237
+ const options = getSearchTitleOptions(sharedValue);
238
+ navigation?.instance?.setOptions({
239
+ header: undefined,
240
+ ...frameOptions,
241
+ ...options,
242
+ });
243
+ }, [navigation?.instance, sharedValue, frameOptions]);
243
244
 
244
- // the search field spans the content, clear of the side bar and status bar clusters
245
+ // the search field spans the content (or the stretch frame), clear of the side bar
246
+ // and status bar clusters
245
247
  const setSearchHeader = (params: SearchHeaderProps) => {
246
- let searchWidth = layout.width - 24;
248
+ let searchWidth = headerLayout.frameWidth;
247
249
  let searchLeft = Spacing.M;
248
250
  if (layout.sideBar) {
249
251
  searchWidth = layout.contentWidth - Spacing.M * 2;
@@ -254,6 +256,7 @@ export const useScreenHeader = ({
254
256
  searchWidth -= clearance.left + clearance.right;
255
257
  }
256
258
  const options: StackNavigationOptions = {
259
+ ...frameOptions,
257
260
  headerRight: undefined,
258
261
  headerTitleContainerStyle: {
259
262
  width: searchWidth,
@@ -9,6 +9,7 @@ import {
9
9
  import { FloatingButtonProps } from './FloatingButton';
10
10
  import { HeaderType } from './types';
11
11
  import { InputRef } from '../Input';
12
+ import type { HeaderContentLayout } from '../Application/headerLayout';
12
13
 
13
14
  export const ANIMATED_BANNER_HEIGHT = 210;
14
15
 
@@ -59,8 +60,9 @@ export interface ScreenProps extends ViewProps {
59
60
 
60
61
  /**
61
62
  * Optional. If `true`, the footer is inset by the standard horizontal margin
62
- * (12pt each side); `false` spans the full width, e.g. for a footer with its
63
- * own horizontal padding. Defaults to `true`, on every platform.
63
+ * (12pt each side). Set `false` for a footer that should span the full width —
64
+ * e.g. one that already carries its own horizontal padding.
65
+ * Defaults to `true`, on every platform.
64
66
  */
65
67
  marginFooter?: boolean;
66
68
 
@@ -76,10 +78,17 @@ export interface ScreenProps extends ViewProps {
76
78
  layoutOffset?: -8 | -24 | -56 | number;
77
79
 
78
80
  /**
79
- * Optional. Width of the right header. Defaults to the toolkit width (73), or 0 when
81
+ * Optional. Width of the right header. Defaults to the toolkit width (84), or 0 when
80
82
  * iPhone Duo floats the toolkit into its side bar.
81
83
  */
82
- headerRightWidth?: 0 | 73 | 110 | number;
84
+ headerRightWidth?: 0 | 84 | 124 | number;
85
+
86
+ /**
87
+ * Optional. `stretch` centers back, title or search and the right cluster of the
88
+ * header in a 600pt frame on wide windows (foldable inner display, tablet); the
89
+ * background and status bar stay full width. Defaults to `grid`.
90
+ */
91
+ contentLayout?: HeaderContentLayout;
83
92
 
84
93
  /**
85
94
  * Optional. Props for the floating action button.
@@ -13,8 +13,6 @@ import { ApplicationContext, MiniAppContext } from '../Context';
13
13
 
14
14
  const INDICATOR_WIDTH = 24;
15
15
  const INDICATOR_CONTAINER_WIDTH = 72;
16
- // one frame at 60 fps
17
- const SCROLL_EVENT_THROTTLE = 16;
18
16
 
19
17
  /**
20
18
  * Horizontal scroll view with a small indicator that tracks the scroll offset.
@@ -33,7 +31,7 @@ const PaginationScroll: FC<ScrollIndicatorProps> = ({
33
31
 
34
32
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
35
33
 
36
- const handleScroll = useAnimatedScrollHandler({
34
+ const onScroll = useAnimatedScrollHandler({
37
35
  onScroll: (event) => {
38
36
  left.value = event.contentOffset.x;
39
37
  },
@@ -56,8 +54,8 @@ const PaginationScroll: FC<ScrollIndicatorProps> = ({
56
54
  return (
57
55
  <Animated.ScrollView
58
56
  ref={scrollViewRef as any}
59
- onScroll={handleScroll}
60
- scrollEventThrottle={SCROLL_EVENT_THROTTLE}
57
+ onScroll={onScroll}
58
+ scrollEventThrottle={16}
61
59
  alwaysBounceHorizontal={false}
62
60
  showsHorizontalScrollIndicator={false}
63
61
  horizontal
@@ -3,9 +3,11 @@ import {Ref} from 'react';
3
3
 
4
4
  export type PaginationProps = {
5
5
  /**
6
- * Optional. Visual style: 'default' dots, 'black_white' dots on a translucent
7
- * dark background, 'number' page numbers. 'scroll' renders as 'default'; use
8
- * `PaginationScroll` instead.
6
+ * Optional. Defines the visual style of the Pagination component.
7
+ * - 'default': Represents the default style.
8
+ * - 'black_white': Represents dots on a translucent dark background.
9
+ * - 'number': Represents a style where pages are represented by numbers.
10
+ * - 'scroll': Rendered as 'default'; use `PaginationScroll` instead.
9
11
  */
10
12
  type?: 'default' | 'black_white' | 'number' | 'scroll';
11
13
 
@@ -12,9 +12,6 @@ import { ModalIconButton } from '../Application/ModalIconButton';
12
12
  import { splitHighlightText } from './utils';
13
13
 
14
14
  const MAX_ROW_TITLE_LENGTH = 12;
15
- const DESCRIPTION_MAX_HEIGHT = 172;
16
- const MAX_DESCRIPTION_LINES = 8.5;
17
- const IMAGE_ASPECT_RATIO = 1.7777;
18
15
 
19
16
  /**
20
17
  * Notify popup body: optional image, title, description, error code and one or
@@ -35,10 +32,10 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
35
32
  }) => {
36
33
  const context = useContext<any>(MiniAppContext);
37
34
  const { theme, navigator, translate } = useContext(ApplicationContext);
38
- const [isDescriptionScrollable, setIsDescriptionScrollable] = useState(false);
35
+ const [scrollContent, setScrollContent] = useState(false);
39
36
  const { isFoldable, popup } = useModalLayout();
40
37
  const { width: popupWidth, maxHeight, radius } = popup;
41
- const scaledMaxHeight = useScaleSize(DESCRIPTION_MAX_HEIGHT);
38
+ const scaledMaxHeight = useScaleSize(172);
42
39
 
43
40
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
44
41
  const closeAction = useRef('touch_outside');
@@ -48,12 +45,12 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
48
45
  // foldables cap the popup height, the description takes what is left
49
46
  Description = ScrollView;
50
47
  descriptionStyle = styles.shrink;
51
- } else if (isDescriptionScrollable) {
48
+ } else if (scrollContent) {
52
49
  Description = ScrollView;
53
50
  descriptionStyle = { maxHeight: scaledMaxHeight };
54
51
  }
55
52
 
56
- const handleAction = (callback?: () => void) => {
53
+ const onAction = (callback?: () => void) => {
57
54
  if (typeof onRequestClose === 'function') {
58
55
  onRequestClose?.(callback);
59
56
  } else {
@@ -75,7 +72,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
75
72
  size="medium"
76
73
  onPress={() => {
77
74
  closeAction.current = 'button_action';
78
- handleAction(secondary?.onPress);
75
+ onAction(secondary?.onPress);
79
76
  }}
80
77
  accessibilityLabel={'btn_popup_cancel'}
81
78
  />
@@ -89,7 +86,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
89
86
  size="medium"
90
87
  onPress={() => {
91
88
  closeAction.current = 'button_action';
92
- handleAction(primary.onPress);
89
+ onAction(primary.onPress);
93
90
  }}
94
91
  accessibilityLabel={'btn_popup_allow'}
95
92
  />
@@ -106,7 +103,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
106
103
  size="medium"
107
104
  onPress={() => {
108
105
  closeAction.current = 'button_action';
109
- handleAction(primary.onPress);
106
+ onAction(primary.onPress);
110
107
  }}
111
108
  />
112
109
  {!!secondary?.title && (
@@ -119,7 +116,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
119
116
  size="medium"
120
117
  onPress={() => {
121
118
  closeAction.current = 'button_action';
122
- handleAction(secondary?.onPress);
119
+ onAction(secondary?.onPress);
123
120
  }}
124
121
  />
125
122
  </>
@@ -153,7 +150,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
153
150
  return text;
154
151
  }
155
152
  return segments.map((segment, index) => {
156
- if (segment.isHighlighted) {
153
+ if (segment.highlight) {
157
154
  return (
158
155
  <Text key={index} typography={'header_s_semibold'}>
159
156
  {segment.text}
@@ -175,7 +172,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
175
172
  style={styles.iconClose}
176
173
  onPress={() => {
177
174
  closeAction.current = 'icon_close';
178
- handleAction(onIconClose);
175
+ onAction(onIconClose);
179
176
  }}
180
177
  />
181
178
  );
@@ -192,9 +189,9 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
192
189
  accessibilityLabel={'description_popup_notify'}
193
190
  typography={'body_default_regular'}
194
191
  onTextLayout={({ nativeEvent: { lines } }) => {
195
- const shouldScroll = lines.length > MAX_DESCRIPTION_LINES;
196
- if (shouldScroll !== isDescriptionScrollable) {
197
- setIsDescriptionScrollable(shouldScroll);
192
+ const enableScroll = lines.length > 8.5;
193
+ if (enableScroll !== scrollContent) {
194
+ setScrollContent(enableScroll);
198
195
  }
199
196
  }}
200
197
  >
@@ -263,7 +260,7 @@ const styles = StyleSheet.create({
263
260
  // clips the image and content to the card radius
264
261
  card: { overflow: 'hidden' },
265
262
  image: {
266
- aspectRatio: IMAGE_ASPECT_RATIO,
263
+ aspectRatio: 1.7777,
267
264
  },
268
265
  shrink: { flexShrink: 1 },
269
266
  content: { padding: Spacing.XL },
@@ -9,7 +9,6 @@ import { useModalLayout } from '../Application/modalLayout';
9
9
 
10
10
  const ASPECT_RATIO = 0.72;
11
11
  const ICON_CLOSE_SIZE = 20;
12
- const CLOSE_GLYPH_SIZE = 14;
13
12
  // close button row under the image: marginTop + vertical padding + icon
14
13
  const CLOSE_ROW_HEIGHT = Spacing.S * 3 + ICON_CLOSE_SIZE;
15
14
 
@@ -38,7 +37,7 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
38
37
 
39
38
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
40
39
 
41
- const handleAction = (callback?: () => void) => {
40
+ const onAction = (callback?: () => void) => {
42
41
  if (typeof onRequestClose === 'function') {
43
42
  onRequestClose?.(callback);
44
43
  } else {
@@ -54,7 +53,7 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
54
53
  accessibilityLabel={'ic_popup_close'}
55
54
  onPress={() => {
56
55
  closeAction.current = 'icon_close';
57
- handleAction(onIconClose);
56
+ onAction(onIconClose);
58
57
  }}
59
58
  style={styles.iconButton}
60
59
  >
@@ -70,7 +69,7 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
70
69
  <Icon
71
70
  source="navigation_close"
72
71
  color={theme.colors.background.surface}
73
- size={CLOSE_GLYPH_SIZE}
72
+ size={14}
74
73
  />
75
74
  </View>
76
75
  </TouchableOpacity>
@@ -83,7 +82,7 @@ const PopupPromotion: React.FC<PopupPromotionProps> = ({
83
82
  accessibilityLabel={'popup_promotion'}
84
83
  style={[{ width: imageWidth }, showBaseLineDebug && styles.debugBaseLine]}
85
84
  >
86
- <TouchableOpacity onPress={() => handleAction(onActionClick)}>
85
+ <TouchableOpacity onPress={() => onAction(onActionClick)}>
87
86
  <Image
88
87
  source={{
89
88
  uri: image,
package/Popup/types.ts CHANGED
@@ -43,8 +43,10 @@ export type PopupNotifyProps = {
43
43
  primary: PopupAction;
44
44
 
45
45
  /**
46
- * `buttonDirection`: Optional. Layout of the action buttons: 'row' horizontal,
47
- * 'column' vertical, 'auto' column only when a title is too long for a row.
46
+ * `buttonDirection`: Optional. Represents the direction in which the action buttons are laid out.
47
+ * - 'row': Buttons are laid out horizontally.
48
+ * - 'column': Buttons are laid out vertically.
49
+ * - 'auto': The layout is automatically determined by title length.
48
50
  */
49
51
  buttonDirection?: PopupActionDirection;
50
52
 
package/Popup/utils.ts CHANGED
@@ -1,4 +1,4 @@
1
- type HighlightSegment = { text: string; isHighlighted: boolean };
1
+ type HighlightSegment = { text: string; highlight: boolean };
2
2
 
3
3
  /**
4
4
  * True for cased-script letters, digits and `_`; caseless scripts (CJK, Thai)
@@ -46,15 +46,12 @@ const splitHighlightText = (
46
46
  continue;
47
47
  }
48
48
  if (start > lastIndex) {
49
- segments.push({
50
- text: text.slice(lastIndex, start),
51
- isHighlighted: false,
52
- });
49
+ segments.push({ text: text.slice(lastIndex, start), highlight: false });
53
50
  }
54
- segments.push({ text: match[0], isHighlighted: true });
51
+ segments.push({ text: match[0], highlight: true });
55
52
  lastIndex = end;
56
53
  }
57
- segments.push({ text: text.slice(lastIndex), isHighlighted: false });
54
+ segments.push({ text: text.slice(lastIndex), highlight: false });
58
55
  return segments;
59
56
  };
60
57
 
@@ -21,12 +21,6 @@ import { Colors, Styles } from '../Consts';
21
21
  import styles from './styles';
22
22
  import { ScreenContext, SkeletonContext } from '../Context';
23
23
 
24
- const PRIMARY_COLOR = Colors.black_05;
25
- const HIGHLIGHT_COLOR1 = Colors.black_05;
26
- const HIGHLIGHT_COLOR2 = Colors.black_03;
27
- const SHIMMER_DURATION = 1000;
28
- const LAYOUT_ANIMATION_DURATION = 500;
29
-
30
24
  /**
31
25
  * Shimmering placeholder that fills its parent. Reports loading to the screen
32
26
  * context while mounted.
@@ -34,6 +28,9 @@ const LAYOUT_ANIMATION_DURATION = 500;
34
28
  const Skeleton: React.FC<SkeletonTypes> = ({ style }) => {
35
29
  const screen = useContext<any>(ScreenContext);
36
30
  const [width, setWidth] = useState(0);
31
+ const PRIMARY_COLOR = Colors.black_05;
32
+ const HIGHLIGHT_COLOR1 = Colors.black_05;
33
+ const HIGHLIGHT_COLOR2 = Colors.black_03;
37
34
  const progress = useSharedValue(0);
38
35
 
39
36
  const shimmerColors = [HIGHLIGHT_COLOR1, HIGHLIGHT_COLOR2, HIGHLIGHT_COLOR1];
@@ -42,7 +39,7 @@ const Skeleton: React.FC<SkeletonTypes> = ({ style }) => {
42
39
  progress.value = 0;
43
40
  progress.value = withRepeat(
44
41
  withTiming(1, {
45
- duration: SHIMMER_DURATION,
42
+ duration: 1000,
46
43
  easing: Easing.linear,
47
44
  }),
48
45
  -1,
@@ -60,7 +57,7 @@ const Skeleton: React.FC<SkeletonTypes> = ({ style }) => {
60
57
  return { transform: [{ translateX }] };
61
58
  });
62
59
 
63
- const handleLayout = (newWidth: number) => {
60
+ const onLayout = (newWidth: number) => {
64
61
  if (newWidth !== width) {
65
62
  setWidth(newWidth);
66
63
  }
@@ -69,7 +66,7 @@ const Skeleton: React.FC<SkeletonTypes> = ({ style }) => {
69
66
  return (
70
67
  <View style={[styles.container, style]}>
71
68
  <View
72
- onLayout={e => handleLayout(e.nativeEvent.layout.width)}
69
+ onLayout={e => onLayout(e.nativeEvent.layout.width)}
73
70
  style={[Styles.flex, { backgroundColor: PRIMARY_COLOR }]}
74
71
  >
75
72
  <Animated.View style={[styles.shimmer, shimmerStyle]}>
@@ -99,28 +96,28 @@ const SkeletonProvider: React.FC<{ loading: boolean; children: any }> = ({
99
96
  loading = true,
100
97
  children,
101
98
  }) => {
102
- const [isLoadingShown, setIsLoadingShown] = useState(loading);
99
+ const [previous, setPrevious] = useState(loading);
103
100
 
104
101
  useEffect(() => {
105
- if (isLoadingShown !== loading) {
102
+ if (previous !== loading) {
106
103
  if (Platform.OS === 'android') {
107
104
  if (UIManager.setLayoutAnimationEnabledExperimental) {
108
105
  UIManager.setLayoutAnimationEnabledExperimental(true);
109
106
  }
110
107
  }
111
108
  LayoutAnimation.configureNext({
112
- duration: LAYOUT_ANIMATION_DURATION,
109
+ duration: 500,
113
110
  create: {
114
111
  type: LayoutAnimation.Types.easeInEaseOut,
115
112
  property: LayoutAnimation.Properties.opacity,
116
113
  },
117
114
  });
118
- setIsLoadingShown(loading);
115
+ setPrevious(loading);
119
116
  }
120
- }, [loading, isLoadingShown]);
117
+ }, [loading, previous]);
121
118
 
122
119
  return (
123
- <SkeletonContext.Provider value={{ loading: isLoadingShown }}>
120
+ <SkeletonContext.Provider value={{ loading: previous }}>
124
121
  {children}
125
122
  </SkeletonContext.Provider>
126
123
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.169.9",
3
+ "version": "0.169.10-beta.1",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {},