@hero-design/rn 8.131.1 → 8.131.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @hero-design/rn
2
2
 
3
+ ## 8.131.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#5161](https://github.com/Thinkei/hero-design/pull/5161) [`f9dd6c0a09ca33eac8455c8d03bbbf420f9b6d27`](https://github.com/Thinkei/hero-design/commit/f9dd6c0a09ca33eac8455c8d03bbbf420f9b6d27) Thanks [@ttkien](https://github.com/ttkien)! - [Tabs] Fix white space in Tab.Scroll header
8
+
3
9
  ## 8.131.1
4
10
 
5
11
  ### Patch Changes
package/es/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as reactNative from 'react-native';
2
- import { StyleSheet as StyleSheet$1, Platform, Dimensions, Keyboard, Animated, View, UIManager, LayoutAnimation, TouchableOpacity, Text as Text$1, Easing, useWindowDimensions, TouchableWithoutFeedback, Modal as Modal$1, Image as Image$1, Pressable, KeyboardAvoidingView, TouchableHighlight, ScrollView, FlatList, TextInput as TextInput$1, PanResponder, BackHandler, InteractionManager, SectionList, RefreshControl as RefreshControl$1 } from 'react-native';
2
+ import { StyleSheet as StyleSheet$1, Platform, Dimensions, Keyboard, Animated, View, UIManager, LayoutAnimation, TouchableOpacity, Text as Text$1, Easing, useWindowDimensions, TouchableWithoutFeedback, Modal as Modal$1, Image as Image$1, Pressable, KeyboardAvoidingView, TouchableHighlight, ScrollView, FlatList, TextInput as TextInput$1, PanResponder, BackHandler, InteractionManager, SectionList, PixelRatio, RefreshControl as RefreshControl$1 } from 'react-native';
3
3
  import * as React from 'react';
4
4
  import React__default, { useState, useEffect, useMemo, useCallback, useRef, useLayoutEffect, createContext, forwardRef, useContext, memo, useReducer, isValidElement, useImperativeHandle } from 'react';
5
5
  import MaskedView from '@react-native-masked-view/masked-view';
@@ -28157,7 +28157,7 @@ var HeaderTabPillBody = index$c(Animated.View)(function (_ref4) {
28157
28157
  top: 0,
28158
28158
  bottom: 0,
28159
28159
  left: 0,
28160
- width: 1,
28160
+ width: 1 / PixelRatio.get(),
28161
28161
  backgroundColor: theme.__hd__.tabs.colors.highlightedActiveBackground
28162
28162
  };
28163
28163
  });
@@ -28178,7 +28178,7 @@ var HeaderTabItemActiveBorder = index$c(Animated.View)(function (_ref6) {
28178
28178
  return {
28179
28179
  position: 'absolute',
28180
28180
  bottom: 0,
28181
- width: 1,
28181
+ width: 1 / PixelRatio.get(),
28182
28182
  height: theme.__hd__.tabs.borderWidths.highlightedActiveBorder,
28183
28183
  backgroundColor: theme.__hd__.tabs.colors.highlightedActiveBorder
28184
28184
  };
@@ -28196,7 +28196,7 @@ var HeaderTabItemWrapper = index$c(View)(function (_ref7) {
28196
28196
  var HeaderTabItemIndicator = index$c(Animated.View)(function (_ref8) {
28197
28197
  var theme = _ref8.theme;
28198
28198
  return {
28199
- width: 1,
28199
+ width: 1 / PixelRatio.get(),
28200
28200
  height: theme.__hd__.tabs.sizes.indicator,
28201
28201
  position: 'absolute',
28202
28202
  bottom: theme.__hd__.tabs.space.tabIndicatorBottom,
@@ -28277,44 +28277,17 @@ var TabWithBadge = function TabWithBadge(_ref) {
28277
28277
  return /*#__PURE__*/React__default.createElement(View, null, tabItem);
28278
28278
  };
28279
28279
 
28280
- /**
28281
- * Drives two visual layers that slide to the selected tab on every press:
28282
- *
28283
- * Layer 1 — bottom border / underline (indicatorStyle)
28284
- * ─────────────────────────────────────────────────────
28285
- * Uses the "width:1 + scaleX" trick: the element has a fixed stylesheet
28286
- * width of 1px and scaleX is set to the target pixel width, giving a visual
28287
- * width of 1 × scaleX pixels without touching any layout property.
28288
- * Both translateX and scaleX are transform properties → native driver.
28289
- * Caveat: scaleX also scales border-radius, so this layer has no border-radius.
28290
- *
28291
- * Layer 2 — pill background (pillLeftStyle / pillBodyStyle / pillRightStyle)
28292
- * ─────────────────────────────────────────────────────────────────────────────
28293
- * The pill is split into three absolutely-positioned children so that
28294
- * border-radius is never distorted by scale:
28295
- *
28296
- * ┌──────────────────────────────────────────────────────┐
28297
- * │ [cap-left 8px] [body width-1 + scaleX] [cap-right 8px] │
28298
- * └──────────────────────────────────────────────────────┘
28299
- *
28300
- * cap-left — fixed 8px wide, borderTopLeftRadius:8, translateX = pillX
28301
- * body — width:1 + scaleX trick (scaleX = tabWidth - 16),
28302
- * transformOrigin 'left center',
28303
- * translateX = pillX + 8 (via Animated.add)
28304
- * cap-right — fixed 8px wide, borderTopRightRadius:8,
28305
- * translateX = pillX + tabWidth - 8 (via Animated.add)
28306
- *
28307
- * All four animated values use the native driver (translateX and scaleX are
28308
- * transform properties). `width` is never animated, so no JS driver needed.
28309
- *
28310
- * Driver summary:
28311
- * indicatorX native translateX — slides the bottom border
28312
- * indicatorScaleX native scaleX — stretches the bottom border
28313
- * pillX native translateX — slides all three pill pieces
28314
- * pillBodyScaleX native scaleX — stretches the pill body
28315
- * pillRightOffsetX native translateX — positions the right cap
28316
- * (Animated.add: pillX + tabWidth - 8)
28317
- */
28280
+ /** Centralised calculation for all animated values given a tab layout. */
28281
+ var computeValues = function computeValues(layout, pillCapWidth) {
28282
+ var pr = PixelRatio.get();
28283
+ return {
28284
+ indicatorX: layout.x,
28285
+ indicatorScaleX: layout.width * pr,
28286
+ pillX: layout.x,
28287
+ pillBodyScaleX: Math.max(layout.width - pillCapWidth * 2, 0) * pr,
28288
+ pillRightOffset: Math.max(layout.width - pillCapWidth, 0)
28289
+ };
28290
+ };
28318
28291
  var useIndicatorAnimation = function useIndicatorAnimation(_ref) {
28319
28292
  var selectedIndex = _ref.selectedIndex,
28320
28293
  tabsLength = _ref.tabsLength,
@@ -28323,9 +28296,6 @@ var useIndicatorAnimation = function useIndicatorAnimation(_ref) {
28323
28296
  var indicatorX = React__default.useRef(new Animated.Value(0)).current;
28324
28297
  var indicatorScaleX = React__default.useRef(new Animated.Value(1)).current;
28325
28298
  // Layer 2 — native driver (pill background, three-piece split).
28326
- // pillX: left edge of the pill (shared by all three pieces as base).
28327
- // pillBodyScaleX: scaleX for the body piece (tabWidth - 2 * CAP_WIDTH).
28328
- // pillRightOffset: additional x offset for the right cap (tabWidth - CAP_WIDTH).
28329
28299
  var pillX = React__default.useRef(new Animated.Value(0)).current;
28330
28300
  var pillBodyScaleX = React__default.useRef(new Animated.Value(1)).current;
28331
28301
  var pillRightOffset = React__default.useRef(new Animated.Value(0)).current;
@@ -28348,44 +28318,30 @@ var useIndicatorAnimation = function useIndicatorAnimation(_ref) {
28348
28318
  if (!layout) return;
28349
28319
  (_runningAnimRef$curre = runningAnimRef.current) === null || _runningAnimRef$curre === void 0 || _runningAnimRef$curre.stop();
28350
28320
  runningAnimRef.current = null;
28351
- // Layer 1: bottom-border element has width:1, so scaleX = pixel width.
28352
- var indicatorScaleXValue = layout.width;
28353
- // Layer 2 body: width:1 element, scaleX fills space between the two caps.
28354
- var bodyScaleX = Math.max(layout.width - pillCapWidth * 2, 0);
28355
- // Layer 2 right cap: offset from pillX to reach the right edge.
28356
- // Clamped to 0 so the right cap never slides left of the pill's origin
28357
- // when the tab is narrower than one cap width.
28358
- var rightOffset = Math.max(layout.width - pillCapWidth, 0);
28321
+ var values = computeValues(layout, pillCapWidth);
28359
28322
  if (!animate || !initializedRef.current) {
28360
- // First render — snap all values immediately without animation.
28361
- indicatorX.setValue(layout.x);
28362
- indicatorScaleX.setValue(indicatorScaleXValue);
28363
- pillX.setValue(layout.x);
28364
- pillBodyScaleX.setValue(bodyScaleX);
28365
- pillRightOffset.setValue(rightOffset);
28323
+ indicatorX.setValue(values.indicatorX);
28324
+ indicatorScaleX.setValue(values.indicatorScaleX);
28325
+ pillX.setValue(values.pillX);
28326
+ pillBodyScaleX.setValue(values.pillBodyScaleX);
28327
+ pillRightOffset.setValue(values.pillRightOffset);
28366
28328
  initializedRef.current = true;
28367
28329
  return;
28368
28330
  }
28369
- // All five animations run on the native driver (UI thread):
28370
- // indicatorX — slides the bottom border
28371
- // indicatorScaleX — stretches the bottom border
28372
- // pillX — slides all three pill pieces together
28373
- // pillBodyScaleX — resizes the body piece to fill between caps
28374
- // pillRightOffset — keeps the right cap at the pill's right edge
28375
28331
  var anim = Animated.parallel([Animated.timing(indicatorX, {
28376
- toValue: layout.x,
28332
+ toValue: values.indicatorX,
28377
28333
  useNativeDriver: true
28378
28334
  }), Animated.timing(indicatorScaleX, {
28379
- toValue: indicatorScaleXValue,
28335
+ toValue: values.indicatorScaleX,
28380
28336
  useNativeDriver: true
28381
28337
  }), Animated.timing(pillX, {
28382
- toValue: layout.x,
28338
+ toValue: values.pillX,
28383
28339
  useNativeDriver: true
28384
28340
  }), Animated.timing(pillBodyScaleX, {
28385
- toValue: bodyScaleX,
28341
+ toValue: values.pillBodyScaleX,
28386
28342
  useNativeDriver: true
28387
28343
  }), Animated.timing(pillRightOffset, {
28388
- toValue: rightOffset,
28344
+ toValue: values.pillRightOffset,
28389
28345
  useNativeDriver: true
28390
28346
  })]);
28391
28347
  runningAnimRef.current = anim;
@@ -28400,7 +28356,6 @@ var useIndicatorAnimation = function useIndicatorAnimation(_ref) {
28400
28356
  if (layoutsRef.current[selectedIndex]) {
28401
28357
  animateTo(selectedIndex, initializedRef.current);
28402
28358
  } else {
28403
- // Layout not yet measured — store as pending and resolve in onTabLayout.
28404
28359
  pendingIndexRef.current = selectedIndex;
28405
28360
  }
28406
28361
  }, [selectedIndex, animateTo]);
@@ -28412,9 +28367,8 @@ var useIndicatorAnimation = function useIndicatorAnimation(_ref) {
28412
28367
  };
28413
28368
  }, []);
28414
28369
  var onTabLayout = React__default.useCallback(function (index, event) {
28415
- var _event$nativeEvent$la = event.nativeEvent.layout,
28416
- x = _event$nativeEvent$la.x,
28417
- width = _event$nativeEvent$la.width;
28370
+ var x = PixelRatio.roundToNearestPixel(event.nativeEvent.layout.x);
28371
+ var width = PixelRatio.roundToNearestPixel(event.nativeEvent.layout.width);
28418
28372
  var prev = layoutsRef.current[index];
28419
28373
  // Skip if layout hasn't meaningfully changed (sub-pixel tolerance).
28420
28374
  if (prev && Math.abs(prev.x - x) < 0.5 && Math.abs(prev.width - width) < 0.5) {
@@ -28424,23 +28378,24 @@ var useIndicatorAnimation = function useIndicatorAnimation(_ref) {
28424
28378
  x: x,
28425
28379
  width: width
28426
28380
  };
28427
- // Animate if this tab is the selected one (covers the pending case where
28428
- // selectedIndex was set before the layout was measured).
28429
28381
  if (index === selectedIndex || index === pendingIndexRef.current) {
28430
28382
  if (index === pendingIndexRef.current) pendingIndexRef.current = undefined;
28431
28383
  animateTo(index, initializedRef.current);
28432
28384
  }
28433
- // If no tab is selected yet, snap indicators to tab 0 on its first
28434
- // layout so they appear at a sensible default position.
28385
+ // If no tab is selected yet, snap indicators to tab 0 on its first layout.
28435
28386
  if (!initializedRef.current && index === 0 && selectedIndex === undefined) {
28436
- indicatorScaleX.setValue(width);
28437
- pillX.setValue(x);
28438
- pillBodyScaleX.setValue(Math.max(width - pillCapWidth * 2, 0));
28439
- pillRightOffset.setValue(Math.max(width - pillCapWidth, 0));
28387
+ var values = computeValues({
28388
+ x: x,
28389
+ width: width
28390
+ }, pillCapWidth);
28391
+ indicatorX.setValue(values.indicatorX);
28392
+ indicatorScaleX.setValue(values.indicatorScaleX);
28393
+ pillX.setValue(values.pillX);
28394
+ pillBodyScaleX.setValue(values.pillBodyScaleX);
28395
+ pillRightOffset.setValue(values.pillRightOffset);
28440
28396
  initializedRef.current = true;
28441
28397
  }
28442
- }, [animateTo, selectedIndex, pillCapWidth]);
28443
- // Layer 1: transformOrigin 'left center' pins scaleX expansion to left edge.
28398
+ }, [selectedIndex, animateTo, pillCapWidth, indicatorX, indicatorScaleX, pillX, pillBodyScaleX, pillRightOffset]);
28444
28399
  var indicatorStyle = {
28445
28400
  transformOrigin: 'left center',
28446
28401
  transform: [{
@@ -28449,8 +28404,6 @@ var useIndicatorAnimation = function useIndicatorAnimation(_ref) {
28449
28404
  scaleX: indicatorScaleX
28450
28405
  }]
28451
28406
  };
28452
- // Layer 2: three pieces, all absolutely positioned, all native driver.
28453
- // Animated.add computes derived positions without creating JS-driver nodes.
28454
28407
  var pillLeftStyle = {
28455
28408
  transform: [{
28456
28409
  translateX: pillX
package/lib/index.js CHANGED
@@ -28186,7 +28186,7 @@ var HeaderTabPillBody = index$c(reactNative.Animated.View)(function (_ref4) {
28186
28186
  top: 0,
28187
28187
  bottom: 0,
28188
28188
  left: 0,
28189
- width: 1,
28189
+ width: 1 / reactNative.PixelRatio.get(),
28190
28190
  backgroundColor: theme.__hd__.tabs.colors.highlightedActiveBackground
28191
28191
  };
28192
28192
  });
@@ -28207,7 +28207,7 @@ var HeaderTabItemActiveBorder = index$c(reactNative.Animated.View)(function (_re
28207
28207
  return {
28208
28208
  position: 'absolute',
28209
28209
  bottom: 0,
28210
- width: 1,
28210
+ width: 1 / reactNative.PixelRatio.get(),
28211
28211
  height: theme.__hd__.tabs.borderWidths.highlightedActiveBorder,
28212
28212
  backgroundColor: theme.__hd__.tabs.colors.highlightedActiveBorder
28213
28213
  };
@@ -28225,7 +28225,7 @@ var HeaderTabItemWrapper = index$c(reactNative.View)(function (_ref7) {
28225
28225
  var HeaderTabItemIndicator = index$c(reactNative.Animated.View)(function (_ref8) {
28226
28226
  var theme = _ref8.theme;
28227
28227
  return {
28228
- width: 1,
28228
+ width: 1 / reactNative.PixelRatio.get(),
28229
28229
  height: theme.__hd__.tabs.sizes.indicator,
28230
28230
  position: 'absolute',
28231
28231
  bottom: theme.__hd__.tabs.space.tabIndicatorBottom,
@@ -28306,44 +28306,17 @@ var TabWithBadge = function TabWithBadge(_ref) {
28306
28306
  return /*#__PURE__*/React__namespace.default.createElement(reactNative.View, null, tabItem);
28307
28307
  };
28308
28308
 
28309
- /**
28310
- * Drives two visual layers that slide to the selected tab on every press:
28311
- *
28312
- * Layer 1 — bottom border / underline (indicatorStyle)
28313
- * ─────────────────────────────────────────────────────
28314
- * Uses the "width:1 + scaleX" trick: the element has a fixed stylesheet
28315
- * width of 1px and scaleX is set to the target pixel width, giving a visual
28316
- * width of 1 × scaleX pixels without touching any layout property.
28317
- * Both translateX and scaleX are transform properties → native driver.
28318
- * Caveat: scaleX also scales border-radius, so this layer has no border-radius.
28319
- *
28320
- * Layer 2 — pill background (pillLeftStyle / pillBodyStyle / pillRightStyle)
28321
- * ─────────────────────────────────────────────────────────────────────────────
28322
- * The pill is split into three absolutely-positioned children so that
28323
- * border-radius is never distorted by scale:
28324
- *
28325
- * ┌──────────────────────────────────────────────────────┐
28326
- * │ [cap-left 8px] [body width-1 + scaleX] [cap-right 8px] │
28327
- * └──────────────────────────────────────────────────────┘
28328
- *
28329
- * cap-left — fixed 8px wide, borderTopLeftRadius:8, translateX = pillX
28330
- * body — width:1 + scaleX trick (scaleX = tabWidth - 16),
28331
- * transformOrigin 'left center',
28332
- * translateX = pillX + 8 (via Animated.add)
28333
- * cap-right — fixed 8px wide, borderTopRightRadius:8,
28334
- * translateX = pillX + tabWidth - 8 (via Animated.add)
28335
- *
28336
- * All four animated values use the native driver (translateX and scaleX are
28337
- * transform properties). `width` is never animated, so no JS driver needed.
28338
- *
28339
- * Driver summary:
28340
- * indicatorX native translateX — slides the bottom border
28341
- * indicatorScaleX native scaleX — stretches the bottom border
28342
- * pillX native translateX — slides all three pill pieces
28343
- * pillBodyScaleX native scaleX — stretches the pill body
28344
- * pillRightOffsetX native translateX — positions the right cap
28345
- * (Animated.add: pillX + tabWidth - 8)
28346
- */
28309
+ /** Centralised calculation for all animated values given a tab layout. */
28310
+ var computeValues = function computeValues(layout, pillCapWidth) {
28311
+ var pr = reactNative.PixelRatio.get();
28312
+ return {
28313
+ indicatorX: layout.x,
28314
+ indicatorScaleX: layout.width * pr,
28315
+ pillX: layout.x,
28316
+ pillBodyScaleX: Math.max(layout.width - pillCapWidth * 2, 0) * pr,
28317
+ pillRightOffset: Math.max(layout.width - pillCapWidth, 0)
28318
+ };
28319
+ };
28347
28320
  var useIndicatorAnimation = function useIndicatorAnimation(_ref) {
28348
28321
  var selectedIndex = _ref.selectedIndex,
28349
28322
  tabsLength = _ref.tabsLength,
@@ -28352,9 +28325,6 @@ var useIndicatorAnimation = function useIndicatorAnimation(_ref) {
28352
28325
  var indicatorX = React__namespace.default.useRef(new reactNative.Animated.Value(0)).current;
28353
28326
  var indicatorScaleX = React__namespace.default.useRef(new reactNative.Animated.Value(1)).current;
28354
28327
  // Layer 2 — native driver (pill background, three-piece split).
28355
- // pillX: left edge of the pill (shared by all three pieces as base).
28356
- // pillBodyScaleX: scaleX for the body piece (tabWidth - 2 * CAP_WIDTH).
28357
- // pillRightOffset: additional x offset for the right cap (tabWidth - CAP_WIDTH).
28358
28328
  var pillX = React__namespace.default.useRef(new reactNative.Animated.Value(0)).current;
28359
28329
  var pillBodyScaleX = React__namespace.default.useRef(new reactNative.Animated.Value(1)).current;
28360
28330
  var pillRightOffset = React__namespace.default.useRef(new reactNative.Animated.Value(0)).current;
@@ -28377,44 +28347,30 @@ var useIndicatorAnimation = function useIndicatorAnimation(_ref) {
28377
28347
  if (!layout) return;
28378
28348
  (_runningAnimRef$curre = runningAnimRef.current) === null || _runningAnimRef$curre === void 0 || _runningAnimRef$curre.stop();
28379
28349
  runningAnimRef.current = null;
28380
- // Layer 1: bottom-border element has width:1, so scaleX = pixel width.
28381
- var indicatorScaleXValue = layout.width;
28382
- // Layer 2 body: width:1 element, scaleX fills space between the two caps.
28383
- var bodyScaleX = Math.max(layout.width - pillCapWidth * 2, 0);
28384
- // Layer 2 right cap: offset from pillX to reach the right edge.
28385
- // Clamped to 0 so the right cap never slides left of the pill's origin
28386
- // when the tab is narrower than one cap width.
28387
- var rightOffset = Math.max(layout.width - pillCapWidth, 0);
28350
+ var values = computeValues(layout, pillCapWidth);
28388
28351
  if (!animate || !initializedRef.current) {
28389
- // First render — snap all values immediately without animation.
28390
- indicatorX.setValue(layout.x);
28391
- indicatorScaleX.setValue(indicatorScaleXValue);
28392
- pillX.setValue(layout.x);
28393
- pillBodyScaleX.setValue(bodyScaleX);
28394
- pillRightOffset.setValue(rightOffset);
28352
+ indicatorX.setValue(values.indicatorX);
28353
+ indicatorScaleX.setValue(values.indicatorScaleX);
28354
+ pillX.setValue(values.pillX);
28355
+ pillBodyScaleX.setValue(values.pillBodyScaleX);
28356
+ pillRightOffset.setValue(values.pillRightOffset);
28395
28357
  initializedRef.current = true;
28396
28358
  return;
28397
28359
  }
28398
- // All five animations run on the native driver (UI thread):
28399
- // indicatorX — slides the bottom border
28400
- // indicatorScaleX — stretches the bottom border
28401
- // pillX — slides all three pill pieces together
28402
- // pillBodyScaleX — resizes the body piece to fill between caps
28403
- // pillRightOffset — keeps the right cap at the pill's right edge
28404
28360
  var anim = reactNative.Animated.parallel([reactNative.Animated.timing(indicatorX, {
28405
- toValue: layout.x,
28361
+ toValue: values.indicatorX,
28406
28362
  useNativeDriver: true
28407
28363
  }), reactNative.Animated.timing(indicatorScaleX, {
28408
- toValue: indicatorScaleXValue,
28364
+ toValue: values.indicatorScaleX,
28409
28365
  useNativeDriver: true
28410
28366
  }), reactNative.Animated.timing(pillX, {
28411
- toValue: layout.x,
28367
+ toValue: values.pillX,
28412
28368
  useNativeDriver: true
28413
28369
  }), reactNative.Animated.timing(pillBodyScaleX, {
28414
- toValue: bodyScaleX,
28370
+ toValue: values.pillBodyScaleX,
28415
28371
  useNativeDriver: true
28416
28372
  }), reactNative.Animated.timing(pillRightOffset, {
28417
- toValue: rightOffset,
28373
+ toValue: values.pillRightOffset,
28418
28374
  useNativeDriver: true
28419
28375
  })]);
28420
28376
  runningAnimRef.current = anim;
@@ -28429,7 +28385,6 @@ var useIndicatorAnimation = function useIndicatorAnimation(_ref) {
28429
28385
  if (layoutsRef.current[selectedIndex]) {
28430
28386
  animateTo(selectedIndex, initializedRef.current);
28431
28387
  } else {
28432
- // Layout not yet measured — store as pending and resolve in onTabLayout.
28433
28388
  pendingIndexRef.current = selectedIndex;
28434
28389
  }
28435
28390
  }, [selectedIndex, animateTo]);
@@ -28441,9 +28396,8 @@ var useIndicatorAnimation = function useIndicatorAnimation(_ref) {
28441
28396
  };
28442
28397
  }, []);
28443
28398
  var onTabLayout = React__namespace.default.useCallback(function (index, event) {
28444
- var _event$nativeEvent$la = event.nativeEvent.layout,
28445
- x = _event$nativeEvent$la.x,
28446
- width = _event$nativeEvent$la.width;
28399
+ var x = reactNative.PixelRatio.roundToNearestPixel(event.nativeEvent.layout.x);
28400
+ var width = reactNative.PixelRatio.roundToNearestPixel(event.nativeEvent.layout.width);
28447
28401
  var prev = layoutsRef.current[index];
28448
28402
  // Skip if layout hasn't meaningfully changed (sub-pixel tolerance).
28449
28403
  if (prev && Math.abs(prev.x - x) < 0.5 && Math.abs(prev.width - width) < 0.5) {
@@ -28453,23 +28407,24 @@ var useIndicatorAnimation = function useIndicatorAnimation(_ref) {
28453
28407
  x: x,
28454
28408
  width: width
28455
28409
  };
28456
- // Animate if this tab is the selected one (covers the pending case where
28457
- // selectedIndex was set before the layout was measured).
28458
28410
  if (index === selectedIndex || index === pendingIndexRef.current) {
28459
28411
  if (index === pendingIndexRef.current) pendingIndexRef.current = undefined;
28460
28412
  animateTo(index, initializedRef.current);
28461
28413
  }
28462
- // If no tab is selected yet, snap indicators to tab 0 on its first
28463
- // layout so they appear at a sensible default position.
28414
+ // If no tab is selected yet, snap indicators to tab 0 on its first layout.
28464
28415
  if (!initializedRef.current && index === 0 && selectedIndex === undefined) {
28465
- indicatorScaleX.setValue(width);
28466
- pillX.setValue(x);
28467
- pillBodyScaleX.setValue(Math.max(width - pillCapWidth * 2, 0));
28468
- pillRightOffset.setValue(Math.max(width - pillCapWidth, 0));
28416
+ var values = computeValues({
28417
+ x: x,
28418
+ width: width
28419
+ }, pillCapWidth);
28420
+ indicatorX.setValue(values.indicatorX);
28421
+ indicatorScaleX.setValue(values.indicatorScaleX);
28422
+ pillX.setValue(values.pillX);
28423
+ pillBodyScaleX.setValue(values.pillBodyScaleX);
28424
+ pillRightOffset.setValue(values.pillRightOffset);
28469
28425
  initializedRef.current = true;
28470
28426
  }
28471
- }, [animateTo, selectedIndex, pillCapWidth]);
28472
- // Layer 1: transformOrigin 'left center' pins scaleX expansion to left edge.
28427
+ }, [selectedIndex, animateTo, pillCapWidth, indicatorX, indicatorScaleX, pillX, pillBodyScaleX, pillRightOffset]);
28473
28428
  var indicatorStyle = {
28474
28429
  transformOrigin: 'left center',
28475
28430
  transform: [{
@@ -28478,8 +28433,6 @@ var useIndicatorAnimation = function useIndicatorAnimation(_ref) {
28478
28433
  scaleX: indicatorScaleX
28479
28434
  }]
28480
28435
  };
28481
- // Layer 2: three pieces, all absolutely positioned, all native driver.
28482
- // Animated.add computes derived positions without creating JS-driver nodes.
28483
28436
  var pillLeftStyle = {
28484
28437
  transform: [{
28485
28438
  translateX: pillX
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hero-design/rn",
3
- "version": "8.131.1",
3
+ "version": "8.131.2",
4
4
  "license": "MIT",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.js",
@@ -12,8 +12,6 @@ export type IconName = typeof IconList[number];
12
12
  export interface IconProps extends AccessibilityProps {
13
13
  /**
14
14
  * Name of the Icon.
15
- *
16
- * icon['carat-*'] - @deprecated Icons starting with 'carat' are deprecated and will be removed in the next major release, please use 'caret' instead.
17
15
  */
18
16
  icon: IconName;
19
17
  /**
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Animated } from 'react-native';
2
+ import { Animated, PixelRatio } from 'react-native';
3
3
  import type { LayoutChangeEvent } from 'react-native';
4
4
 
5
5
  type Layout = { x: number; width: number };
@@ -9,9 +9,9 @@ type Layout = { x: number; width: number };
9
9
  *
10
10
  * Layer 1 — bottom border / underline (indicatorStyle)
11
11
  * ─────────────────────────────────────────────────────
12
- * Uses the "width:1 + scaleX" trick: the element has a fixed stylesheet
13
- * width of 1px and scaleX is set to the target pixel width, giving a visual
14
- * width of 1 × scaleX pixels without touching any layout property.
12
+ * Uses the "width:1/PixelRatio + scaleX" trick: the element has a stylesheet
13
+ * width of 1 physical pixel (1/PixelRatio dp) and scaleX is set to
14
+ * targetWidth * PixelRatio, giving a visual width of targetWidth dp.
15
15
  * Both translateX and scaleX are transform properties → native driver.
16
16
  * Caveat: scaleX also scales border-radius, so this layer has no border-radius.
17
17
  *
@@ -21,27 +21,43 @@ type Layout = { x: number; width: number };
21
21
  * border-radius is never distorted by scale:
22
22
  *
23
23
  * ┌──────────────────────────────────────────────────────┐
24
- * │ [cap-left 8px] [body width-1 + scaleX] [cap-right 8px] │
24
+ * │ [cap-left cap] [body 1px + scaleX] [cap-right cap] │
25
25
  * └──────────────────────────────────────────────────────┘
26
26
  *
27
- * cap-left — fixed 8px wide, borderTopLeftRadius:8, translateX = pillX
28
- * body — width:1 + scaleX trick (scaleX = tabWidth - 16),
29
- * transformOrigin 'left center',
30
- * translateX = pillX + 8 (via Animated.add)
31
- * cap-right fixed 8px wide, borderTopRightRadius:8,
32
- * translateX = pillX + tabWidth - 8 (via Animated.add)
33
- *
34
- * All four animated values use the native driver (translateX and scaleX are
35
- * transform properties). `width` is never animated, so no JS driver needed.
27
+ * cap-left — fixed pillCapWidth dp, borderTopLeftRadius, translateX = pillX
28
+ * body — 1px + scaleX trick (scaleX = (tabWidth - 2*cap) * PixelRatio),
29
+ * translateX = pillX + pillCapWidth (via Animated.add)
30
+ * cap-right fixed pillCapWidth dp, borderTopRightRadius,
31
+ * translateX = pillX + rightOffset (via Animated.add)
36
32
  *
37
33
  * Driver summary:
38
- * indicatorX native translateX — slides the bottom border
39
- * indicatorScaleX native scaleX — stretches the bottom border
40
- * pillX native translateX — slides all three pill pieces
41
- * pillBodyScaleX native scaleX — stretches the pill body
42
- * pillRightOffsetX native translateX — positions the right cap
43
- * (Animated.add: pillX + tabWidth - 8)
34
+ * indicatorX native translateX — slides the bottom border
35
+ * indicatorScaleX native scaleX — stretches the bottom border
36
+ * pillX native translateX — slides all three pill pieces
37
+ * pillBodyScaleX native scaleX — stretches the pill body
38
+ * pillRightOffset native translateX — positions the right cap
44
39
  */
40
+
41
+ type AnimValues = {
42
+ indicatorX: number;
43
+ indicatorScaleX: number;
44
+ pillX: number;
45
+ pillBodyScaleX: number;
46
+ pillRightOffset: number;
47
+ };
48
+
49
+ /** Centralised calculation for all animated values given a tab layout. */
50
+ const computeValues = (layout: Layout, pillCapWidth: number): AnimValues => {
51
+ const pr = PixelRatio.get();
52
+ return {
53
+ indicatorX: layout.x,
54
+ indicatorScaleX: layout.width * pr,
55
+ pillX: layout.x,
56
+ pillBodyScaleX: Math.max(layout.width - pillCapWidth * 2, 0) * pr,
57
+ pillRightOffset: Math.max(layout.width - pillCapWidth, 0),
58
+ };
59
+ };
60
+
45
61
  const useIndicatorAnimation = ({
46
62
  selectedIndex,
47
63
  tabsLength,
@@ -57,9 +73,6 @@ const useIndicatorAnimation = ({
57
73
  const indicatorScaleX = React.useRef(new Animated.Value(1)).current;
58
74
 
59
75
  // Layer 2 — native driver (pill background, three-piece split).
60
- // pillX: left edge of the pill (shared by all three pieces as base).
61
- // pillBodyScaleX: scaleX for the body piece (tabWidth - 2 * CAP_WIDTH).
62
- // pillRightOffset: additional x offset for the right cap (tabWidth - CAP_WIDTH).
63
76
  const pillX = React.useRef(new Animated.Value(0)).current;
64
77
  const pillBodyScaleX = React.useRef(new Animated.Value(1)).current;
65
78
  const pillRightOffset = React.useRef(new Animated.Value(0)).current;
@@ -86,51 +99,37 @@ const useIndicatorAnimation = ({
86
99
  runningAnimRef.current?.stop();
87
100
  runningAnimRef.current = null;
88
101
 
89
- // Layer 1: bottom-border element has width:1, so scaleX = pixel width.
90
- const indicatorScaleXValue = layout.width;
91
- // Layer 2 body: width:1 element, scaleX fills space between the two caps.
92
- const bodyScaleX = Math.max(layout.width - pillCapWidth * 2, 0);
93
- // Layer 2 right cap: offset from pillX to reach the right edge.
94
- // Clamped to 0 so the right cap never slides left of the pill's origin
95
- // when the tab is narrower than one cap width.
96
- const rightOffset = Math.max(layout.width - pillCapWidth, 0);
102
+ const values = computeValues(layout, pillCapWidth);
97
103
 
98
104
  if (!animate || !initializedRef.current) {
99
- // First render — snap all values immediately without animation.
100
- indicatorX.setValue(layout.x);
101
- indicatorScaleX.setValue(indicatorScaleXValue);
102
- pillX.setValue(layout.x);
103
- pillBodyScaleX.setValue(bodyScaleX);
104
- pillRightOffset.setValue(rightOffset);
105
+ indicatorX.setValue(values.indicatorX);
106
+ indicatorScaleX.setValue(values.indicatorScaleX);
107
+ pillX.setValue(values.pillX);
108
+ pillBodyScaleX.setValue(values.pillBodyScaleX);
109
+ pillRightOffset.setValue(values.pillRightOffset);
105
110
  initializedRef.current = true;
106
111
  return;
107
112
  }
108
113
 
109
- // All five animations run on the native driver (UI thread):
110
- // indicatorX — slides the bottom border
111
- // indicatorScaleX — stretches the bottom border
112
- // pillX — slides all three pill pieces together
113
- // pillBodyScaleX — resizes the body piece to fill between caps
114
- // pillRightOffset — keeps the right cap at the pill's right edge
115
114
  const anim = Animated.parallel([
116
115
  Animated.timing(indicatorX, {
117
- toValue: layout.x,
116
+ toValue: values.indicatorX,
118
117
  useNativeDriver: true,
119
118
  }),
120
119
  Animated.timing(indicatorScaleX, {
121
- toValue: indicatorScaleXValue,
120
+ toValue: values.indicatorScaleX,
122
121
  useNativeDriver: true,
123
122
  }),
124
123
  Animated.timing(pillX, {
125
- toValue: layout.x,
124
+ toValue: values.pillX,
126
125
  useNativeDriver: true,
127
126
  }),
128
127
  Animated.timing(pillBodyScaleX, {
129
- toValue: bodyScaleX,
128
+ toValue: values.pillBodyScaleX,
130
129
  useNativeDriver: true,
131
130
  }),
132
131
  Animated.timing(pillRightOffset, {
133
- toValue: rightOffset,
132
+ toValue: values.pillRightOffset,
134
133
  useNativeDriver: true,
135
134
  }),
136
135
  ]);
@@ -155,7 +154,6 @@ const useIndicatorAnimation = ({
155
154
  if (layoutsRef.current[selectedIndex]) {
156
155
  animateTo(selectedIndex, initializedRef.current);
157
156
  } else {
158
- // Layout not yet measured — store as pending and resolve in onTabLayout.
159
157
  pendingIndexRef.current = selectedIndex;
160
158
  }
161
159
  }, [selectedIndex, animateTo]);
@@ -169,7 +167,10 @@ const useIndicatorAnimation = ({
169
167
 
170
168
  const onTabLayout = React.useCallback(
171
169
  (index: number, event: LayoutChangeEvent) => {
172
- const { x, width } = event.nativeEvent.layout;
170
+ const x = PixelRatio.roundToNearestPixel(event.nativeEvent.layout.x);
171
+ const width = PixelRatio.roundToNearestPixel(
172
+ event.nativeEvent.layout.width
173
+ );
173
174
  const prev = layoutsRef.current[index];
174
175
  // Skip if layout hasn't meaningfully changed (sub-pixel tolerance).
175
176
  if (
@@ -181,39 +182,44 @@ const useIndicatorAnimation = ({
181
182
  }
182
183
  layoutsRef.current[index] = { x, width };
183
184
 
184
- // Animate if this tab is the selected one (covers the pending case where
185
- // selectedIndex was set before the layout was measured).
186
185
  if (index === selectedIndex || index === pendingIndexRef.current) {
187
186
  if (index === pendingIndexRef.current)
188
187
  pendingIndexRef.current = undefined;
189
188
  animateTo(index, initializedRef.current);
190
189
  }
191
190
 
192
- // If no tab is selected yet, snap indicators to tab 0 on its first
193
- // layout so they appear at a sensible default position.
191
+ // If no tab is selected yet, snap indicators to tab 0 on its first layout.
194
192
  if (
195
193
  !initializedRef.current &&
196
194
  index === 0 &&
197
195
  selectedIndex === undefined
198
196
  ) {
199
- indicatorScaleX.setValue(width);
200
- pillX.setValue(x);
201
- pillBodyScaleX.setValue(Math.max(width - pillCapWidth * 2, 0));
202
- pillRightOffset.setValue(Math.max(width - pillCapWidth, 0));
197
+ const values = computeValues({ x, width }, pillCapWidth);
198
+ indicatorX.setValue(values.indicatorX);
199
+ indicatorScaleX.setValue(values.indicatorScaleX);
200
+ pillX.setValue(values.pillX);
201
+ pillBodyScaleX.setValue(values.pillBodyScaleX);
202
+ pillRightOffset.setValue(values.pillRightOffset);
203
203
  initializedRef.current = true;
204
204
  }
205
205
  },
206
- [animateTo, selectedIndex, pillCapWidth]
206
+ [
207
+ selectedIndex,
208
+ animateTo,
209
+ pillCapWidth,
210
+ indicatorX,
211
+ indicatorScaleX,
212
+ pillX,
213
+ pillBodyScaleX,
214
+ pillRightOffset,
215
+ ]
207
216
  );
208
217
 
209
- // Layer 1: transformOrigin 'left center' pins scaleX expansion to left edge.
210
218
  const indicatorStyle = {
211
219
  transformOrigin: 'left center',
212
220
  transform: [{ translateX: indicatorX }, { scaleX: indicatorScaleX }],
213
221
  } as const;
214
222
 
215
- // Layer 2: three pieces, all absolutely positioned, all native driver.
216
- // Animated.add computes derived positions without creating JS-driver nodes.
217
223
  const pillLeftStyle = {
218
224
  transform: [{ translateX: pillX }],
219
225
  } as const;
@@ -1,4 +1,4 @@
1
- import { Animated, Platform, View } from 'react-native';
1
+ import { Animated, PixelRatio, Platform, View } from 'react-native';
2
2
  import styled from '@emotion/native';
3
3
 
4
4
  // Checks if the platform is Android 7x and 8.x, i.e., API levels 24 to 27 (Android 7.0 to 8.1),
@@ -67,7 +67,7 @@ const HeaderTabPillBody = styled(Animated.View)(({ theme }) => ({
67
67
  top: 0,
68
68
  bottom: 0,
69
69
  left: 0,
70
- width: 1,
70
+ width: 1 / PixelRatio.get(),
71
71
  backgroundColor: theme.__hd__.tabs.colors.highlightedActiveBackground,
72
72
  }));
73
73
 
@@ -84,7 +84,7 @@ const HeaderTabPillRight = styled(Animated.View)(({ theme }) => ({
84
84
  const HeaderTabItemActiveBorder = styled(Animated.View)(({ theme }) => ({
85
85
  position: 'absolute',
86
86
  bottom: 0,
87
- width: 1,
87
+ width: 1 / PixelRatio.get(),
88
88
  height: theme.__hd__.tabs.borderWidths.highlightedActiveBorder,
89
89
  backgroundColor: theme.__hd__.tabs.colors.highlightedActiveBorder,
90
90
  }));
@@ -99,7 +99,7 @@ const HeaderTabItemWrapper = styled(View)(({ theme }) => ({
99
99
  }));
100
100
 
101
101
  const HeaderTabItemIndicator = styled(Animated.View)(({ theme }) => ({
102
- width: 1,
102
+ width: 1 / PixelRatio.get(),
103
103
  height: theme.__hd__.tabs.sizes.indicator,
104
104
  position: 'absolute',
105
105
  bottom: theme.__hd__.tabs.space.tabIndicatorBottom,
@@ -5,8 +5,6 @@ export type IconName = typeof IconList[number];
5
5
  export interface IconProps extends AccessibilityProps {
6
6
  /**
7
7
  * Name of the Icon.
8
- *
9
- * icon['carat-*'] - @deprecated Icons starting with 'carat' are deprecated and will be removed in the next major release, please use 'caret' instead.
10
8
  */
11
9
  icon: IconName;
12
10
  /**
@@ -1,43 +1,5 @@
1
1
  import { Animated } from 'react-native';
2
2
  import type { LayoutChangeEvent } from 'react-native';
3
- /**
4
- * Drives two visual layers that slide to the selected tab on every press:
5
- *
6
- * Layer 1 — bottom border / underline (indicatorStyle)
7
- * ─────────────────────────────────────────────────────
8
- * Uses the "width:1 + scaleX" trick: the element has a fixed stylesheet
9
- * width of 1px and scaleX is set to the target pixel width, giving a visual
10
- * width of 1 × scaleX pixels without touching any layout property.
11
- * Both translateX and scaleX are transform properties → native driver.
12
- * Caveat: scaleX also scales border-radius, so this layer has no border-radius.
13
- *
14
- * Layer 2 — pill background (pillLeftStyle / pillBodyStyle / pillRightStyle)
15
- * ─────────────────────────────────────────────────────────────────────────────
16
- * The pill is split into three absolutely-positioned children so that
17
- * border-radius is never distorted by scale:
18
- *
19
- * ┌──────────────────────────────────────────────────────┐
20
- * │ [cap-left 8px] [body width-1 + scaleX] [cap-right 8px] │
21
- * └──────────────────────────────────────────────────────┘
22
- *
23
- * cap-left — fixed 8px wide, borderTopLeftRadius:8, translateX = pillX
24
- * body — width:1 + scaleX trick (scaleX = tabWidth - 16),
25
- * transformOrigin 'left center',
26
- * translateX = pillX + 8 (via Animated.add)
27
- * cap-right — fixed 8px wide, borderTopRightRadius:8,
28
- * translateX = pillX + tabWidth - 8 (via Animated.add)
29
- *
30
- * All four animated values use the native driver (translateX and scaleX are
31
- * transform properties). `width` is never animated, so no JS driver needed.
32
- *
33
- * Driver summary:
34
- * indicatorX native translateX — slides the bottom border
35
- * indicatorScaleX native scaleX — stretches the bottom border
36
- * pillX native translateX — slides all three pill pieces
37
- * pillBodyScaleX native scaleX — stretches the pill body
38
- * pillRightOffsetX native translateX — positions the right cap
39
- * (Animated.add: pillX + tabWidth - 8)
40
- */
41
3
  declare const useIndicatorAnimation: ({ selectedIndex, tabsLength, pillCapWidth, }: {
42
4
  selectedIndex: number | undefined;
43
5
  tabsLength: number;