@mpxjs/webpack-plugin 2.10.24 → 2.11.0

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 (151) hide show
  1. package/lib/index.js +4 -1
  2. package/lib/platform/style/wx/index.js +417 -120
  3. package/lib/platform/template/wx/component-config/picker.js +0 -6
  4. package/lib/react/style-helper.js +6 -6
  5. package/lib/resolver/AddEnvPlugin.js +1 -1
  6. package/lib/resolver/AddModePlugin.js +1 -1
  7. package/lib/resolver/ExtendComponentsPlugin.js +68 -0
  8. package/lib/runtime/components/extends/section-list.mpx +14 -0
  9. package/lib/runtime/components/react/animationHooks/index.ts +7 -3
  10. package/lib/runtime/components/react/dist/animationHooks/index.d.ts +0 -2
  11. package/lib/runtime/components/react/dist/animationHooks/index.js +6 -1
  12. package/lib/runtime/components/react/dist/event.config.d.ts +2 -6
  13. package/lib/runtime/components/react/dist/event.config.js +28 -24
  14. package/lib/runtime/components/react/dist/getInnerListeners.d.ts +1 -1
  15. package/lib/runtime/components/react/dist/getInnerListeners.js +74 -56
  16. package/lib/runtime/components/react/dist/mpx-button.d.ts +1 -2
  17. package/lib/runtime/components/react/dist/mpx-button.jsx +16 -9
  18. package/lib/runtime/components/react/dist/mpx-camera.d.ts +0 -2
  19. package/lib/runtime/components/react/dist/mpx-camera.jsx +36 -5
  20. package/lib/runtime/components/react/dist/mpx-canvas/index.d.ts +0 -2
  21. package/lib/runtime/components/react/dist/mpx-canvas/index.jsx +1 -3
  22. package/lib/runtime/components/react/dist/mpx-canvas/utils.d.ts +1 -1
  23. package/lib/runtime/components/react/dist/mpx-checkbox-group.d.ts +0 -2
  24. package/lib/runtime/components/react/dist/mpx-checkbox-group.jsx +3 -4
  25. package/lib/runtime/components/react/dist/mpx-checkbox.d.ts +1 -2
  26. package/lib/runtime/components/react/dist/mpx-checkbox.jsx +8 -9
  27. package/lib/runtime/components/react/dist/mpx-form.d.ts +1 -2
  28. package/lib/runtime/components/react/dist/mpx-form.jsx +5 -5
  29. package/lib/runtime/components/react/dist/mpx-icon/index.d.ts +0 -2
  30. package/lib/runtime/components/react/dist/mpx-icon/index.jsx +2 -2
  31. package/lib/runtime/components/react/dist/mpx-image.d.ts +0 -2
  32. package/lib/runtime/components/react/dist/mpx-image.jsx +59 -43
  33. package/lib/runtime/components/react/dist/mpx-inline-text.jsx +13 -4
  34. package/lib/runtime/components/react/dist/mpx-input.d.ts +0 -2
  35. package/lib/runtime/components/react/dist/mpx-input.jsx +16 -8
  36. package/lib/runtime/components/react/dist/mpx-label.d.ts +1 -2
  37. package/lib/runtime/components/react/dist/mpx-label.jsx +5 -6
  38. package/lib/runtime/components/react/dist/mpx-movable-area.d.ts +0 -2
  39. package/lib/runtime/components/react/dist/mpx-movable-area.jsx +3 -3
  40. package/lib/runtime/components/react/dist/mpx-movable-view.d.ts +1 -2
  41. package/lib/runtime/components/react/dist/mpx-movable-view.jsx +6 -6
  42. package/lib/runtime/components/react/dist/mpx-nav.jsx +1 -1
  43. package/lib/runtime/components/react/dist/mpx-picker/index.jsx +72 -23
  44. package/lib/runtime/components/react/dist/mpx-picker/multiSelector.jsx +2 -2
  45. package/lib/runtime/components/react/dist/mpx-picker/type.d.ts +4 -0
  46. package/lib/runtime/components/react/dist/mpx-picker-view/index.d.ts +1 -1
  47. package/lib/runtime/components/react/dist/mpx-picker-view/index.jsx +10 -14
  48. package/lib/runtime/components/react/dist/mpx-picker-view/pickerVIewContext.d.ts +0 -2
  49. package/lib/runtime/components/react/dist/mpx-picker-view/pickerVIewContext.js +0 -5
  50. package/lib/runtime/components/react/dist/mpx-picker-view-column/index.d.ts +1 -1
  51. package/lib/runtime/components/react/dist/mpx-picker-view-column/index.jsx +34 -14
  52. package/lib/runtime/components/react/dist/mpx-picker-view-column/pickerViewColumnItem.d.ts +0 -2
  53. package/lib/runtime/components/react/dist/mpx-picker-view-column/pickerViewColumnItem.jsx +4 -5
  54. package/lib/runtime/components/react/dist/mpx-picker-view-column/pickerViewColumnItemLite.d.ts +0 -2
  55. package/lib/runtime/components/react/dist/mpx-picker-view-column/pickerViewColumnItemLite.jsx +3 -5
  56. package/lib/runtime/components/react/dist/mpx-popup/index.d.ts +5 -4
  57. package/lib/runtime/components/react/dist/mpx-popup/index.jsx +2 -2
  58. package/lib/runtime/components/react/dist/mpx-popup/popupBase.jsx +3 -2
  59. package/lib/runtime/components/react/dist/mpx-progress.d.ts +0 -2
  60. package/lib/runtime/components/react/dist/mpx-progress.jsx +1 -3
  61. package/lib/runtime/components/react/dist/mpx-radio-group.d.ts +0 -2
  62. package/lib/runtime/components/react/dist/mpx-radio-group.jsx +3 -4
  63. package/lib/runtime/components/react/dist/mpx-radio.d.ts +1 -2
  64. package/lib/runtime/components/react/dist/mpx-radio.jsx +8 -9
  65. package/lib/runtime/components/react/dist/mpx-rich-text/index.d.ts +0 -2
  66. package/lib/runtime/components/react/dist/mpx-rich-text/index.jsx +1 -3
  67. package/lib/runtime/components/react/dist/mpx-scroll-view.d.ts +1 -2
  68. package/lib/runtime/components/react/dist/mpx-scroll-view.jsx +6 -6
  69. package/lib/runtime/components/react/dist/mpx-section-list.d.ts +51 -0
  70. package/lib/runtime/components/react/dist/mpx-section-list.jsx +376 -0
  71. package/lib/runtime/components/react/dist/mpx-simple-text.jsx +37 -19
  72. package/lib/runtime/components/react/dist/mpx-simple-view.d.ts +4 -1
  73. package/lib/runtime/components/react/dist/mpx-simple-view.jsx +11 -4
  74. package/lib/runtime/components/react/dist/mpx-slider.d.ts +0 -2
  75. package/lib/runtime/components/react/dist/mpx-slider.jsx +6 -7
  76. package/lib/runtime/components/react/dist/mpx-sticky-header.d.ts +1 -2
  77. package/lib/runtime/components/react/dist/mpx-sticky-header.jsx +5 -5
  78. package/lib/runtime/components/react/dist/mpx-sticky-section.d.ts +1 -2
  79. package/lib/runtime/components/react/dist/mpx-sticky-section.jsx +6 -6
  80. package/lib/runtime/components/react/dist/mpx-swiper-item.d.ts +1 -2
  81. package/lib/runtime/components/react/dist/mpx-swiper-item.jsx +5 -5
  82. package/lib/runtime/components/react/dist/mpx-swiper.d.ts +1 -2
  83. package/lib/runtime/components/react/dist/mpx-swiper.jsx +4 -8
  84. package/lib/runtime/components/react/dist/mpx-switch.d.ts +0 -2
  85. package/lib/runtime/components/react/dist/mpx-switch.jsx +1 -3
  86. package/lib/runtime/components/react/dist/mpx-text.d.ts +0 -2
  87. package/lib/runtime/components/react/dist/mpx-text.jsx +43 -25
  88. package/lib/runtime/components/react/dist/mpx-video.d.ts +0 -2
  89. package/lib/runtime/components/react/dist/mpx-video.jsx +4 -5
  90. package/lib/runtime/components/react/dist/mpx-view.d.ts +2 -2
  91. package/lib/runtime/components/react/dist/mpx-view.jsx +214 -114
  92. package/lib/runtime/components/react/dist/mpx-web-view.d.ts +2 -1
  93. package/lib/runtime/components/react/dist/mpx-web-view.jsx +5 -1
  94. package/lib/runtime/components/react/dist/namedColorSet.d.ts +2 -0
  95. package/lib/runtime/components/react/dist/namedColorSet.js +149 -0
  96. package/lib/runtime/components/react/dist/utils.d.ts +71 -70
  97. package/lib/runtime/components/react/dist/utils.jsx +1295 -456
  98. package/lib/runtime/components/react/event.config.ts +32 -25
  99. package/lib/runtime/components/react/getInnerListeners.ts +84 -60
  100. package/lib/runtime/components/react/mpx-button.tsx +18 -18
  101. package/lib/runtime/components/react/mpx-camera.tsx +40 -8
  102. package/lib/runtime/components/react/mpx-canvas/index.tsx +5 -7
  103. package/lib/runtime/components/react/mpx-canvas/utils.tsx +3 -1
  104. package/lib/runtime/components/react/mpx-checkbox-group.tsx +2 -8
  105. package/lib/runtime/components/react/mpx-checkbox.tsx +9 -13
  106. package/lib/runtime/components/react/mpx-form.tsx +6 -8
  107. package/lib/runtime/components/react/mpx-icon/index.tsx +1 -5
  108. package/lib/runtime/components/react/mpx-image.tsx +66 -58
  109. package/lib/runtime/components/react/mpx-inline-text.tsx +13 -4
  110. package/lib/runtime/components/react/mpx-input.tsx +19 -12
  111. package/lib/runtime/components/react/mpx-label.tsx +6 -10
  112. package/lib/runtime/components/react/mpx-movable-area.tsx +3 -5
  113. package/lib/runtime/components/react/mpx-movable-view.tsx +7 -9
  114. package/lib/runtime/components/react/mpx-nav.tsx +1 -1
  115. package/lib/runtime/components/react/mpx-picker/index.tsx +100 -29
  116. package/lib/runtime/components/react/mpx-picker/multiSelector.tsx +3 -3
  117. package/lib/runtime/components/react/mpx-picker/selector.tsx +1 -1
  118. package/lib/runtime/components/react/mpx-picker/type.ts +4 -0
  119. package/lib/runtime/components/react/mpx-picker-view/index.tsx +17 -24
  120. package/lib/runtime/components/react/mpx-picker-view/pickerVIewContext.ts +0 -9
  121. package/lib/runtime/components/react/mpx-picker-view-column/index.tsx +44 -23
  122. package/lib/runtime/components/react/mpx-picker-view-column/pickerViewColumnItem.tsx +1 -9
  123. package/lib/runtime/components/react/mpx-picker-view-column/pickerViewColumnItemLite.tsx +0 -9
  124. package/lib/runtime/components/react/mpx-popup/index.tsx +7 -3
  125. package/lib/runtime/components/react/mpx-popup/popupBase.tsx +3 -2
  126. package/lib/runtime/components/react/mpx-progress.tsx +0 -6
  127. package/lib/runtime/components/react/mpx-radio-group.tsx +2 -8
  128. package/lib/runtime/components/react/mpx-radio.tsx +9 -13
  129. package/lib/runtime/components/react/mpx-rich-text/index.tsx +0 -6
  130. package/lib/runtime/components/react/mpx-scroll-view.tsx +7 -9
  131. package/lib/runtime/components/react/mpx-section-list.tsx +557 -0
  132. package/lib/runtime/components/react/mpx-simple-text.tsx +36 -25
  133. package/lib/runtime/components/react/mpx-simple-view.tsx +17 -5
  134. package/lib/runtime/components/react/mpx-slider.tsx +15 -13
  135. package/lib/runtime/components/react/mpx-sticky-header.tsx +6 -8
  136. package/lib/runtime/components/react/mpx-sticky-section.tsx +7 -9
  137. package/lib/runtime/components/react/mpx-swiper-item.tsx +6 -7
  138. package/lib/runtime/components/react/mpx-swiper.tsx +5 -11
  139. package/lib/runtime/components/react/mpx-switch.tsx +0 -6
  140. package/lib/runtime/components/react/mpx-text.tsx +45 -36
  141. package/lib/runtime/components/react/mpx-video.tsx +3 -8
  142. package/lib/runtime/components/react/mpx-view.tsx +235 -129
  143. package/lib/runtime/components/react/mpx-web-view.tsx +12 -6
  144. package/lib/runtime/components/react/namedColorSet.ts +150 -0
  145. package/lib/runtime/components/react/types/getInnerListeners.d.ts +21 -1
  146. package/lib/runtime/components/react/types/global.d.ts +1 -0
  147. package/lib/runtime/components/react/utils.tsx +1352 -550
  148. package/lib/runtime/i18n.wxs +15 -9
  149. package/lib/style-compiler/strip-conditional.js +19 -4
  150. package/lib/template-compiler/compiler.js +6 -0
  151. package/package.json +6 -6
@@ -1,46 +1,54 @@
1
1
  import { useEffect, useCallback, useMemo, useRef, isValidElement, useContext, useState, createElement } from 'react';
2
2
  import { Image } from 'react-native';
3
- import { isObject, isFunction, isNumber, hasOwn, diffAndCloneA, error, warn } from '@mpxjs/utils';
3
+ import { isObject, isFunction, isNumber, hasOwn, diffAndCloneA, shallowEqual, error, warn } from '@mpxjs/utils';
4
4
  import { VarContext, ScrollViewContext, RouteContext, TextPassThroughContext } from './context';
5
5
  import { ExpressionParser, parseFunc, ReplaceSource } from './parser';
6
6
  import { initialWindowMetrics } from 'react-native-safe-area-context';
7
7
  import { Gesture } from 'react-native-gesture-handler';
8
- export const TEXT_STYLE_MAP = {
8
+ export const percentRegExp = /^\s*-?\d+(\.\d+)?%\s*$/;
9
+ export const svgRegExp = /\.svg(?:[?#].*)?$/i;
10
+ export const hiddenStyle = {
11
+ opacity: 0
12
+ };
13
+ export const isIOS = __mpx_mode__ === 'ios';
14
+ export const isAndroid = __mpx_mode__ === 'android';
15
+ export const isHarmony = __mpx_mode__ === 'harmony';
16
+ export const extendObject = Object.assign;
17
+ const textStyleMap = {
9
18
  color: true,
10
19
  letterSpacing: true,
11
20
  lineHeight: true,
12
21
  includeFontPadding: true,
13
22
  writingDirection: true
14
23
  };
15
- export const PERCENT_REGEX = /^\s*-?\d+(\.\d+)?%\s*$/;
16
- export const URL_REGEX = /^\s*url\(["']?(.*?)["']?\)\s*$/;
17
- export const SVG_REGEXP = /\.svg(?:[?#].*)?$/i;
18
- export const BACKGROUND_STYLE_MAP = {
24
+ const urlRegExp = /^\s*url\(["']?(.*?)["']?\)\s*$/;
25
+ const linearGradientRegExp = /^\s*linear-gradient\(.*\)\s*$/;
26
+ const digitStartRegExp = /^\d/;
27
+ const varDecRegExp = /^--/;
28
+ const varUseRegExp = /var\(/;
29
+ const unoVarDecRegExp = /^--un-/;
30
+ const unoVarUseRegExp = /var\(--un-/;
31
+ const lengthValueRegExp = /^(-?(?:\d+(?:\.\d+)?|\.\d+)(?:rpx|px|%|vw|vh)?|hairlineWidth)$/;
32
+ const DEFAULT_FONT_SIZE = 16;
33
+ // transform: 'rotateX(45deg) ...' 单段拆出 fn 名与括号内值
34
+ const transformFnRegExp = /([/\w]+)\((.+)\)/;
35
+ // boxShadow 子值识别 rpx 单位(仅 rpx 需要换算为 px,其它单位保留原样)
36
+ const rpxSuffixRegExp = /\d+rpx$/;
37
+ // font-family / font 简写解析时统一去掉单/双引号
38
+ const quoteCharRegExp = /["']/g;
39
+ const defaultBoxSizingStyle = {
40
+ boxSizing: 'content-box'
41
+ };
42
+ const backgroundStyleMap = {
19
43
  backgroundImage: true,
20
44
  backgroundSize: true,
21
45
  backgroundRepeat: true,
22
46
  backgroundPosition: true
23
47
  };
24
- export const TEXT_PROPS_MAP = {
48
+ const textPropsMap = {
25
49
  ellipsizeMode: true,
26
50
  numberOfLines: true
27
51
  };
28
- export const DEFAULT_FONT_SIZE = 16;
29
- export const HIDDEN_STYLE = {
30
- opacity: 0
31
- };
32
- export const DEFAULT_BOX_SIZING_STYLE = {
33
- boxSizing: 'content-box'
34
- };
35
- export const isIOS = __mpx_mode__ === 'ios';
36
- export const isAndroid = __mpx_mode__ === 'android';
37
- export const isHarmony = __mpx_mode__ === 'harmony';
38
- const varDecRegExp = /^--/;
39
- const varUseRegExp = /var\(/;
40
- const unoVarDecRegExp = /^--un-/;
41
- const unoVarUseRegExp = /var\(--un-/;
42
- const calcUseRegExp = /calc\(/;
43
- const envUseRegExp = /env\(/;
44
52
  const boxSizingAffectingStyleMap = {
45
53
  padding: true,
46
54
  paddingTop: true,
@@ -51,7 +59,135 @@ const boxSizingAffectingStyleMap = {
51
59
  borderTopWidth: true,
52
60
  borderRightWidth: true,
53
61
  borderBottomWidth: true,
54
- borderLeftWidth: true
62
+ borderLeftWidth: true,
63
+ border: true,
64
+ borderTop: true,
65
+ borderRight: true,
66
+ borderBottom: true,
67
+ borderLeft: true
68
+ };
69
+ const runtimeAbbreviationMap = {
70
+ margin: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'],
71
+ padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
72
+ borderRadius: ['borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius'],
73
+ borderWidth: ['borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'],
74
+ borderColor: ['borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor'],
75
+ border: ['borderWidth', 'borderStyle', 'borderColor'],
76
+ // RN 不支持单边 border-*-style,统一展开到 borderStyle
77
+ // 实测 RN 上当 borderStyle 不为 solid 时单边 border-*-color 不生效,
78
+ // 这里把单边 color 也统一展开到 borderColor 规避(width 不能这样做,否则会覆盖其它三边)
79
+ borderTop: ['borderTopWidth', 'borderStyle', 'borderColor'],
80
+ borderRight: ['borderRightWidth', 'borderStyle', 'borderColor'],
81
+ borderBottom: ['borderBottomWidth', 'borderStyle', 'borderColor'],
82
+ borderLeft: ['borderLeftWidth', 'borderStyle', 'borderColor'],
83
+ flexFlow: ['flexDirection', 'flexWrap'],
84
+ textShadow: ['textShadowOffset.width', 'textShadowOffset.height', 'textShadowRadius', 'textShadowColor'],
85
+ textDecoration: ['textDecorationLine', 'textDecorationStyle', 'textDecorationColor'],
86
+ // gap:CSS 2 槽位(row-gap / column-gap),单值复制行列。RN 原生支持但要求 number。
87
+ // 单值串在 __getStyle 的 transformStyleObj / 编译期 formatValue 已被 __formatValue 换算为 number;
88
+ // 真正以字符串形态进 transformShorthand 的是多值串(如 '10px 20px')—— unitRegExp 不命中、原样透传。
89
+ // 因此进入 runtimeForceExpandCompositeMap,避免「单值透传」捷径放过这类多值串残留。
90
+ // 百分比(`gap: 50%` / `gap: 10px 50%`)由 transformShorthand 写回阶段就地 resolvePercent 落成 number
91
+ gap: ['rowGap', 'columnGap'],
92
+ // inset:4 槽位等价 margin 四值语法;RN 0.74+ 原生支持单值 DimensionValue,单值走 runtimeCompositeStyleMap 短路透传
93
+ inset: ['top', 'right', 'bottom', 'left'],
94
+ // outline: <outline-width> || <outline-style> || <outline-color>,顺序不敏感
95
+ outline: ['outlineWidth', 'outlineStyle', 'outlineColor']
96
+ };
97
+ const runtimeCompositeStyleMap = {
98
+ margin: true,
99
+ padding: true,
100
+ borderRadius: true,
101
+ borderWidth: true,
102
+ borderColor: true,
103
+ gap: true,
104
+ inset: true
105
+ };
106
+ const runtimeUnorderedAbbreviationMap = {
107
+ border: true,
108
+ borderTop: true,
109
+ borderRight: true,
110
+ borderBottom: true,
111
+ borderLeft: true,
112
+ flexFlow: true,
113
+ textShadow: true,
114
+ textDecoration: true,
115
+ // outline 与 border 简写共享同一套缺省值处理
116
+ outline: true
117
+ };
118
+ // 单值简写在 __getStyle 阶段经 __formatValue 把带单位长度换算为 number 后,
119
+ // 需要在 transformShorthand 入口转回字符串 token 交给展开逻辑的 key 集合。
120
+ // margin / padding / borderRadius / borderWidth / borderColor / inset 不在此列:
121
+ // RN 原生支持这些 key 直接吃单值 number,无需展开。
122
+ const runtimeNumericExpandShorthandMap = {
123
+ border: true,
124
+ borderTop: true,
125
+ borderRight: true,
126
+ borderBottom: true,
127
+ borderLeft: true,
128
+ outline: true,
129
+ gap: true
130
+ };
131
+ // CSS border-width: medium 的实测值(各主流浏览器一致取 3px)
132
+ // 与编译期 wx/index.js 同名常量保持一致;调整需两侧一起改
133
+ const BORDER_MEDIUM_WIDTH = 3;
134
+ // 运行时简写槽位缺省值表(与编译期 ShorthandDefaultMap 镜像,仅 CSS quote 形式不同)
135
+ // 值即槽位缺省时追加的补齐值;borderColor / textShadowRadius 因 RN 有内置缺省值不补,不进此表
136
+ // none 清除语义统一保留到 useTransformStyle 末尾处理
137
+ const runtimeShorthandDefaultMap = {
138
+ border: { borderWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
139
+ borderTop: { borderTopWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
140
+ borderRight: { borderRightWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
141
+ borderBottom: { borderBottomWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
142
+ borderLeft: { borderLeftWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
143
+ // outline 与 border 缺省值完全对齐:缺 width → BORDER_MEDIUM_WIDTH;
144
+ // 缺 style → outlineStyle: none,末尾统一转换为 outlineWidth: 0
145
+ outline: { outlineWidth: BORDER_MEDIUM_WIDTH, outlineStyle: 'none' },
146
+ textShadow: { textShadowColor: '#000' }
147
+ // textDecoration / flexFlow 暂不配置,与 RN 默认一致
148
+ };
149
+ const borderStyleMap = {
150
+ // RN 实测仅支持 solid/dotted/dashed;none 作为 CSS 合法值保留到末尾统一转换为 borderWidth: 0
151
+ solid: true,
152
+ dotted: true,
153
+ dashed: true,
154
+ none: true
155
+ };
156
+ const textDecorationLineMap = {
157
+ none: true,
158
+ underline: true,
159
+ 'line-through': true
160
+ };
161
+ const textDecorationStyleMap = {
162
+ solid: true,
163
+ double: true,
164
+ dotted: true,
165
+ dashed: true
166
+ };
167
+ // font 简写 <font-weight> 槽位允许的关键字 / 数值(CSS 子集,与 RN 支持一致)
168
+ const fontWeightMap = {
169
+ bold: true,
170
+ normal: true,
171
+ 100: true,
172
+ 200: true,
173
+ 300: true,
174
+ 400: true,
175
+ 500: true,
176
+ 600: true,
177
+ 700: true,
178
+ 800: true,
179
+ 900: true
180
+ };
181
+ const flexDirectionMap = {
182
+ row: true,
183
+ 'row-reverse': true,
184
+ column: true,
185
+ 'column-reverse': true
186
+ };
187
+ const flexWrapMap = {
188
+ wrap: true,
189
+ nowrap: true,
190
+ 'wrap-reverse': true
55
191
  };
56
192
  const safeAreaInsetMap = {
57
193
  'safe-area-inset-top': 'top',
@@ -59,30 +195,42 @@ const safeAreaInsetMap = {
59
195
  'safe-area-inset-bottom': 'bottom',
60
196
  'safe-area-inset-left': 'left'
61
197
  };
62
- export const extendObject = Object.assign;
63
- export function getDefaultAllowFontScaling() {
64
- return global.__mpx?.config?.rnConfig?.allowFontScaling ?? false;
65
- }
66
- export function transformBoxSizing(style = {}, hasBoxSizingAffectingStyle = false) {
67
- if (hasBoxSizingAffectingStyle && style.boxSizing === undefined) {
68
- style.boxSizing = global.__mpx?.config?.rnConfig?.defaultBoxSizing ?? DEFAULT_BOX_SIZING_STYLE.boxSizing;
69
- }
70
- return style;
71
- }
72
- export function isBoxSizingAffectingStyle(key) {
73
- return hasOwn(boxSizingAffectingStyleMap, key);
74
- }
75
- function isTextStyle(key) {
76
- return hasOwn(TEXT_STYLE_MAP, key) || key.startsWith('font') || key.startsWith('text');
77
- }
78
- function getSafeAreaInset(name, navigation) {
79
- const insets = extendObject({}, initialWindowMetrics?.insets, navigation?.insets);
80
- return insets[safeAreaInsetMap[name]];
81
- }
82
- export function useNavigation() {
83
- const { navigation } = useContext(RouteContext) || {};
84
- return navigation;
85
- }
198
+ const radiusPercentRule = {
199
+ borderTopLeftRadius: 'width',
200
+ borderBottomLeftRadius: 'width',
201
+ borderBottomRightRadius: 'width',
202
+ borderTopRightRadius: 'width',
203
+ borderRadius: 'width'
204
+ };
205
+ const selfPercentRule = extendObject({
206
+ translateX: 'width',
207
+ translateY: 'height'
208
+ }, radiusPercentRule);
209
+ const parentHeightPercentRule = {
210
+ height: true,
211
+ minHeight: true,
212
+ maxHeight: true,
213
+ top: true,
214
+ bottom: true,
215
+ // row-gap 百分比按 CSS 规范基于容器内容区高度;columnGap 不入此表,落默认 parentWidth 分支
216
+ rowGap: true
217
+ };
218
+ const bgRepeatMap = {
219
+ repeat: true,
220
+ 'repeat-x': true,
221
+ 'repeat-y': true,
222
+ 'no-repeat': true
223
+ };
224
+ const bgPositionMap = {
225
+ left: true,
226
+ right: true,
227
+ top: true,
228
+ bottom: true,
229
+ center: true
230
+ };
231
+ // ============================================================
232
+ // generic utility functions
233
+ // ============================================================
86
234
  export function omit(obj, fields) {
87
235
  const shallowCopy = extendObject({}, obj);
88
236
  for (let i = 0; i < fields.length; i += 1) {
@@ -91,35 +239,18 @@ export function omit(obj, fields) {
91
239
  }
92
240
  return shallowCopy;
93
241
  }
94
- /**
95
- * 用法等同于 useEffect,但是会忽略首次执行,只在依赖更新时执行
96
- */
97
- export const useUpdateEffect = (effect, deps) => {
98
- const isMounted = useRef(false);
99
- // for react-refresh
100
- useEffect(() => {
101
- return () => {
102
- isMounted.current = false;
103
- };
104
- }, []);
105
- useEffect(() => {
106
- if (!isMounted.current) {
107
- isMounted.current = true;
108
- }
109
- else {
110
- return effect();
111
- }
112
- }, deps);
113
- };
114
242
  export const parseUrl = (cssUrl = '') => {
115
243
  if (!cssUrl)
116
244
  return;
117
- const match = cssUrl.match(URL_REGEX);
245
+ const match = cssUrl.match(urlRegExp);
118
246
  return match?.[1];
119
247
  };
120
248
  export const getRestProps = (transferProps = {}, originProps = {}, deletePropsKey = []) => {
121
249
  return extendObject({}, transferProps, omit(originProps, deletePropsKey));
122
250
  };
251
+ export function getDefaultAllowFontScaling() {
252
+ return global.__mpx?.config?.rnConfig?.allowFontScaling ?? false;
253
+ }
123
254
  export function isText(ele) {
124
255
  if (isValidElement(ele)) {
125
256
  const displayName = ele.type?.displayName;
@@ -135,86 +266,148 @@ export function isStringChildren(children) {
135
266
  return false;
136
267
  return children.every((child) => typeof child === 'string');
137
268
  }
138
- export function groupBy(obj, callback, group = {}) {
139
- Object.entries(obj).forEach(([key, val]) => {
140
- const groupKey = callback(key, val);
141
- group[groupKey] = group[groupKey] || {};
142
- group[groupKey][key] = val;
143
- });
144
- return group;
145
- }
146
- export function splitStyle(styleObj, sideEffect) {
147
- return groupBy(styleObj, (key, val) => {
148
- sideEffect && sideEffect(key, val);
149
- if (isTextStyle(key)) {
150
- return 'textStyle';
269
+ // 多value解析
270
+ export function parseValues(str, char = ' ') {
271
+ let stack = 0;
272
+ let temp = '';
273
+ const result = [];
274
+ for (let i = 0; i < str.length; i++) {
275
+ if (str[i] === '(') {
276
+ stack++;
277
+ }
278
+ else if (str[i] === ')') {
279
+ stack--;
151
280
  }
152
- else if (hasOwn(BACKGROUND_STYLE_MAP, key)) {
153
- return 'backgroundStyle';
281
+ // 非括号内 或者 非分隔字符且非空
282
+ if (stack !== 0 || str[i] !== char) {
283
+ temp += str[i];
154
284
  }
155
- else {
156
- return 'innerStyle';
285
+ if ((stack === 0 && str[i] === char) || i === str.length - 1) {
286
+ result.push(temp.trim());
287
+ temp = '';
157
288
  }
158
- });
289
+ }
290
+ return result;
159
291
  }
160
- const radiusPercentRule = {
161
- borderTopLeftRadius: 'width',
162
- borderBottomLeftRadius: 'width',
163
- borderBottomRightRadius: 'width',
164
- borderTopRightRadius: 'width',
165
- borderRadius: 'width'
166
- };
167
- const selfPercentRule = extendObject({
168
- translateX: 'width',
169
- translateY: 'height'
170
- }, radiusPercentRule);
171
- const parentHeightPercentRule = {
172
- height: true,
173
- minHeight: true,
174
- maxHeight: true,
175
- top: true,
176
- bottom: true
292
+ export const debounce = (func, delay) => {
293
+ let timer;
294
+ const wrapper = (...args) => {
295
+ timer && clearTimeout(timer);
296
+ timer = setTimeout(() => {
297
+ func(...args);
298
+ }, delay);
299
+ };
300
+ wrapper.clear = () => {
301
+ timer && clearTimeout(timer);
302
+ timer = null;
303
+ };
304
+ return wrapper;
177
305
  };
178
- function resolvePercent(value, key, percentConfig) {
179
- if (!(typeof value === 'string' && PERCENT_REGEX.test(value)))
180
- return value;
181
- let base;
182
- let reason;
183
- if (key === 'fontSize') {
184
- base = percentConfig.parentFontSize;
185
- reason = 'parent-font-size';
186
- }
187
- else if (key === 'lineHeight') {
188
- base = resolvePercent(percentConfig.fontSize, 'fontSize', percentConfig);
189
- reason = 'font-size';
190
- }
191
- else if (selfPercentRule[key]) {
192
- base = percentConfig[selfPercentRule[key]];
193
- reason = selfPercentRule[key];
306
+ // ============================================================
307
+ // style classification & splitting
308
+ // ============================================================
309
+ function isTextStyle(key) {
310
+ return hasOwn(textStyleMap, key) || key.startsWith('font') || key.startsWith('text');
311
+ }
312
+ function isColorValue(token) {
313
+ if (token.startsWith('#') || token.startsWith('rgb(') || token.startsWith('rgba(') || token.startsWith('hsl(') || token.startsWith('hsla('))
314
+ return true;
315
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
316
+ return hasOwn(require('./namedColorSet').default, token.toLowerCase());
317
+ }
318
+ function isLengthValue(token) {
319
+ return lengthValueRegExp.test(token);
320
+ }
321
+ export function isBoxSizingAffectingStyle(key) {
322
+ return hasOwn(boxSizingAffectingStyleMap, key);
323
+ }
324
+ // 仅在调用侧已确认存在 padding / border 等 box-sizing 影响项时调用:
325
+ // 给样式补默认 boxSizing(用户未显式设置时)。是否需要进入由调用侧决定,
326
+ // useTransformStyle / mpx-simple-view / mpx-simple-text 都按 hasBoxSizingAffectingStyle 短路。
327
+ export function transformBoxSizing(style = {}) {
328
+ if (style.boxSizing === undefined) {
329
+ style.boxSizing = global.__mpx?.config?.rnConfig?.defaultBoxSizing ?? defaultBoxSizingStyle.boxSizing;
194
330
  }
195
- else if (parentHeightPercentRule[key]) {
196
- base = percentConfig.parentHeight;
197
- reason = 'parent-height';
331
+ }
332
+ export function splitStyle(styleObj = {}, sideEffect) {
333
+ const keys = Object.keys(styleObj);
334
+ let textStyle;
335
+ let backgroundStyle;
336
+ let innerStyle;
337
+ let firstSpecialIdx = -1;
338
+ for (let i = 0; i < keys.length; i++) {
339
+ const key = keys[i];
340
+ const val = styleObj[key];
341
+ sideEffect && sideEffect(key, val);
342
+ if (isTextStyle(key)) {
343
+ if (firstSpecialIdx < 0)
344
+ firstSpecialIdx = i;
345
+ textStyle = textStyle || {};
346
+ textStyle[key] = val;
347
+ }
348
+ else if (hasOwn(backgroundStyleMap, key)) {
349
+ if (firstSpecialIdx < 0)
350
+ firstSpecialIdx = i;
351
+ backgroundStyle = backgroundStyle || {};
352
+ backgroundStyle[key] = val;
353
+ }
354
+ else if (firstSpecialIdx >= 0) {
355
+ innerStyle = innerStyle || {};
356
+ innerStyle[key] = val;
357
+ }
198
358
  }
199
- else {
200
- base = percentConfig.parentWidth;
201
- reason = 'parent-width';
359
+ if (firstSpecialIdx < 0)
360
+ return { innerStyle: styleObj };
361
+ if (firstSpecialIdx > 0) {
362
+ innerStyle = innerStyle || {};
363
+ for (let i = 0; i < firstSpecialIdx; i++) {
364
+ const key = keys[i];
365
+ innerStyle[key] = styleObj[key];
366
+ }
202
367
  }
203
- if (typeof base !== 'number') {
204
- error(`[${key}] can not contain % unit unless you set [${reason}] with a number for the percent calculation.`);
205
- return value;
368
+ return { textStyle, backgroundStyle, innerStyle };
369
+ }
370
+ export function splitProps(props = {}) {
371
+ const keys = Object.keys(props);
372
+ let textProps;
373
+ let innerProps;
374
+ let firstTextIdx = -1;
375
+ for (let i = 0; i < keys.length; i++) {
376
+ const key = keys[i];
377
+ if (hasOwn(textPropsMap, key)) {
378
+ if (firstTextIdx < 0)
379
+ firstTextIdx = i;
380
+ textProps = textProps || {};
381
+ textProps[key] = props[key];
382
+ }
383
+ else if (firstTextIdx >= 0) {
384
+ innerProps = innerProps || {};
385
+ innerProps[key] = props[key];
386
+ }
206
387
  }
207
- else {
208
- return parseFloat(value) / 100 * base;
388
+ if (firstTextIdx < 0)
389
+ return { innerProps: props };
390
+ if (firstTextIdx > 0) {
391
+ innerProps = innerProps || {};
392
+ for (let i = 0; i < firstTextIdx; i++) {
393
+ const key = keys[i];
394
+ innerProps[key] = props[key];
395
+ }
209
396
  }
397
+ return { textProps, innerProps };
210
398
  }
211
- function transformPercent(styleObj, percentKeyPaths, percentConfig) {
212
- percentKeyPaths.forEach((percentKeyPath) => {
213
- setStyle(styleObj, percentKeyPath, ({ target, key, value }) => {
214
- target[key] = resolvePercent(value, key, percentConfig);
215
- });
216
- });
399
+ export function pickStyle(styleObj = {}, pickedKeys, callback) {
400
+ return pickedKeys.reduce((acc, key) => {
401
+ if (key in styleObj) {
402
+ acc[key] = callback ? callback(key, styleObj[key]) : styleObj[key];
403
+ }
404
+ return acc;
405
+ }, {});
217
406
  }
407
+ // ============================================================
408
+ // style transform pipeline
409
+ // ============================================================
410
+ // --- var ---
218
411
  function resolveVar(input, varContext) {
219
412
  const parsed = parseFunc(input, 'var');
220
413
  const replaced = new ReplaceSource(input);
@@ -253,7 +446,9 @@ function transformVar(styleObj, varKeyPaths, varContext, visitOther) {
253
446
  });
254
447
  });
255
448
  }
449
+ // --- env ---
256
450
  function transformEnv(styleObj, envKeyPaths, navigation) {
451
+ const insets = extendObject({}, initialWindowMetrics?.insets, navigation?.insets);
257
452
  envKeyPaths.forEach((envKeyPath) => {
258
453
  setStyle(styleObj, envKeyPath, ({ target, key, value }) => {
259
454
  const parsed = parseFunc(value, 'env');
@@ -261,13 +456,56 @@ function transformEnv(styleObj, envKeyPaths, navigation) {
261
456
  parsed.forEach(({ start, end, args }) => {
262
457
  const name = args[0];
263
458
  const fallback = args[1] || '';
264
- const value = '' + (getSafeAreaInset(name, navigation) ?? global.__formatValue(fallback));
265
- replaced.replace(start, end - 1, value);
459
+ const inset = insets[safeAreaInsetMap[name]];
460
+ const next = '' + (inset ?? global.__formatValue(fallback));
461
+ replaced.replace(start, end - 1, next);
266
462
  });
267
463
  target[key] = global.__formatValue(replaced.source());
268
464
  });
269
465
  });
270
466
  }
467
+ // --- percent ---
468
+ function resolvePercent(value, key, percentConfig) {
469
+ if (!(typeof value === 'string' && percentRegExp.test(value)))
470
+ return value;
471
+ let base;
472
+ let reason;
473
+ if (key === 'fontSize') {
474
+ base = DEFAULT_FONT_SIZE;
475
+ reason = 'default-font-size';
476
+ }
477
+ else if (key === 'lineHeight') {
478
+ base = resolvePercent(percentConfig.fontSize, 'fontSize', percentConfig);
479
+ reason = 'font-size';
480
+ }
481
+ else if (selfPercentRule[key]) {
482
+ base = percentConfig[selfPercentRule[key]];
483
+ reason = selfPercentRule[key];
484
+ }
485
+ else if (parentHeightPercentRule[key]) {
486
+ base = percentConfig.parentHeight;
487
+ reason = 'parent-height';
488
+ }
489
+ else {
490
+ base = percentConfig.parentWidth;
491
+ reason = 'parent-width';
492
+ }
493
+ if (typeof base !== 'number') {
494
+ error(`[${key}] can not contain % unit unless you set [${reason}] with a number for the percent calculation.`);
495
+ return value;
496
+ }
497
+ else {
498
+ return parseFloat(value) / 100 * base;
499
+ }
500
+ }
501
+ function transformPercent(styleObj, percentKeyPaths, percentConfig) {
502
+ percentKeyPaths.forEach((percentKeyPath) => {
503
+ setStyle(styleObj, percentKeyPath, ({ target, key, value }) => {
504
+ target[key] = resolvePercent(value, key, percentConfig);
505
+ });
506
+ });
507
+ }
508
+ // --- calc ---
271
509
  function transformCalc(styleObj, calcKeyPaths, formatter) {
272
510
  calcKeyPaths.forEach((calcKeyPath) => {
273
511
  setStyle(styleObj, calcKeyPath, ({ target, key, value }) => {
@@ -289,6 +527,7 @@ function transformCalc(styleObj, calcKeyPaths, formatter) {
289
527
  });
290
528
  });
291
529
  }
530
+ // --- misc ---
292
531
  function transformStringify(styleObj) {
293
532
  if (isNumber(styleObj.fontWeight)) {
294
533
  styleObj.fontWeight = '' + styleObj.fontWeight;
@@ -298,35 +537,7 @@ function transformStringify(styleObj) {
298
537
  styleObj.transformOrigin = '' + styleObj.transformOrigin;
299
538
  }
300
539
  }
301
- function transformPosition(styleObj, meta) {
302
- if (styleObj.position === 'fixed') {
303
- styleObj.position = 'absolute';
304
- meta.hasPositionFixed = true;
305
- }
306
- }
307
- // 多value解析
308
- export function parseValues(str, char = ' ') {
309
- let stack = 0;
310
- let temp = '';
311
- const result = [];
312
- for (let i = 0; i < str.length; i++) {
313
- if (str[i] === '(') {
314
- stack++;
315
- }
316
- else if (str[i] === ')') {
317
- stack--;
318
- }
319
- // 非括号内 或者 非分隔字符且非空
320
- if (stack !== 0 || str[i] !== char) {
321
- temp += str[i];
322
- }
323
- if ((stack === 0 && str[i] === char) || i === str.length - 1) {
324
- result.push(temp.trim());
325
- temp = '';
326
- }
327
- }
328
- return result;
329
- }
540
+ // --- transform / shadow ---
330
541
  // parse string transform, eg: transform: 'rotateX(45deg) rotateZ(0.785398rad)'
331
542
  function parseTransform(transformStr) {
332
543
  const values = parseValues(transformStr);
@@ -334,69 +545,618 @@ function parseTransform(transformStr) {
334
545
  // Todo 1 transform 排序不一致时,transform动画会闪烁,故这里同样的排序输出 transform
335
546
  // values.sort()
336
547
  const transform = [];
548
+ // 与编译期 wx/index.js 同 transform formatter 口径对齐:
549
+ // 「整条 transform 声明」绝不会因单个子项失败而丢弃,被丢的只有那一个子项,其余 transform 项仍输出。
550
+ // 因此本 forEach 内所有「丢这一项」的提示一律用 warn(编译期同名分支注释:「仅丢这一 transform 项,其它项仍输出,按规范使用 warn」)。
337
551
  values.forEach(item => {
338
- const match = item.match(/([/\w]+)\((.+)\)/);
339
- if (match && match.length >= 3) {
340
- let key = match[1];
341
- const val = match[2];
342
- switch (key) {
343
- case 'translateX':
344
- case 'translateY':
345
- case 'scaleX':
346
- case 'scaleY':
347
- case 'rotateX':
348
- case 'rotateY':
349
- case 'rotateZ':
350
- case 'rotate':
351
- case 'skewX':
352
- case 'skewY':
353
- case 'perspective':
354
- // rotate 处理成 rotateZ
355
- key = key === 'rotate' ? 'rotateZ' : key;
356
- // 单个值处理
357
- transform.push({ [key]: global.__formatValue(val) });
552
+ const match = item.match(transformFnRegExp);
553
+ if (!match || match.length < 3) {
554
+ warn(`Transform value [${item}] is not a valid fn(value) form, dropped.`);
555
+ return;
556
+ }
557
+ let key = match[1];
558
+ const val = match[2];
559
+ switch (key) {
560
+ case 'rotateX':
561
+ case 'rotateY':
562
+ case 'rotateZ':
563
+ case 'rotate':
564
+ case 'skewX':
565
+ case 'skewY':
566
+ key = key === 'rotate' ? 'rotateZ' : key;
567
+ transform.push({ [key]: val });
568
+ break;
569
+ case 'translateX':
570
+ case 'translateY':
571
+ case 'scaleX':
572
+ case 'scaleY':
573
+ case 'perspective':
574
+ transform.push({ [key]: global.__formatValue(val) });
575
+ break;
576
+ case 'matrix': {
577
+ // parseValues 内部已 trim
578
+ const matrixValues = parseValues(val, ',').map(v => +v);
579
+ if (matrixValues.length === 6) {
580
+ const [a, b, c, d, tx, ty] = matrixValues;
581
+ transform.push({ matrix: [a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, tx, ty, 0, 1] });
582
+ }
583
+ else {
584
+ warn(`Transform matrix only supports 6 values in React Native, got ${matrixValues.length}`);
585
+ }
586
+ break;
587
+ }
588
+ case 'matrix3d': {
589
+ // parseValues 内部已 trim
590
+ const matrixValues = parseValues(val, ',').map(v => +v);
591
+ if (matrixValues.length === 16) {
592
+ transform.push({ matrix: matrixValues });
593
+ }
594
+ else {
595
+ warn(`Transform matrix only supports 16 values in React Native, got ${matrixValues.length}`);
596
+ }
597
+ break;
598
+ }
599
+ case 'translate':
600
+ case 'scale':
601
+ case 'skew':
602
+ case 'translate3d': // x y 支持 z不支持
603
+ case 'scale3d': // x y 支持 z不支持
604
+ {
605
+ // 2 个以上的值处理
606
+ key = key.replace('3d', '');
607
+ const vals = parseValues(val, ',').splice(0, 3);
608
+ // scale(.5) === scaleX(.5) scaleY(.5)
609
+ if (vals.length === 1 && key === 'scale') {
610
+ vals.push(vals[0]);
611
+ }
612
+ const xyz = ['X', 'Y', 'Z'];
613
+ vals.forEach((v, index) => {
614
+ if (key !== 'rotate' && index > 1) {
615
+ warn(`Transform [${key}Z] is not supported in React Native, dropped.`);
616
+ return;
617
+ }
618
+ // parseValues 内部已 trim
619
+ transform.push({ [`${key}${xyz[index] || ''}`]: global.__formatValue(v) });
620
+ });
358
621
  break;
359
- case 'matrix':
360
- transform.push({ [key]: parseValues(val, ',').map(val => +val) });
622
+ }
623
+ case 'rotate3d': {
624
+ const parts = parseValues(val, ',');
625
+ if (parts.length !== 4) {
626
+ warn(`Transform rotate3d only supports 4 values, got ${parts.length}`);
361
627
  break;
362
- case 'translate':
363
- case 'scale':
364
- case 'skew':
365
- case 'translate3d': // x y 支持 z不支持
366
- case 'scale3d': // x y 支持 z不支持
367
- {
368
- // 2 个以上的值处理
369
- key = key.replace('3d', '');
370
- const vals = parseValues(val, ',').splice(0, 3);
371
- // scale(.5) === scaleX(.5) scaleY(.5)
372
- if (vals.length === 1 && key === 'scale') {
373
- vals.push(vals[0]);
374
- }
375
- const xyz = ['X', 'Y', 'Z'];
376
- transform.push(...vals.map((v, index) => {
377
- return { [`${key}${xyz[index] || ''}`]: global.__formatValue(v.trim()) };
378
- }));
379
- break;
380
- }
628
+ }
629
+ // parseValues 内部已 trim
630
+ const x = +parts[0];
631
+ const y = +parts[1];
632
+ const z = +parts[2];
633
+ const angle = parts[3];
634
+ if (x && !y && !z)
635
+ transform.push({ rotateX: angle });
636
+ else if (!x && y && !z)
637
+ transform.push({ rotateY: angle });
638
+ else if (!x && !y && z)
639
+ transform.push({ rotateZ: angle });
640
+ else
641
+ warn(`Transform rotate3d(${val}) only supports single-axis vector (1,0,0) / (0,1,0) / (0,0,1) in React Native, dropped.`);
642
+ break;
643
+ }
644
+ case 'translateZ':
645
+ case 'scaleZ':
646
+ // RN 不支持 Z 轴 translate/scale,丢该子项;编译期同分支用 warn(unsupportedPropError isError=false)
647
+ warn(`Transform [${key}] is not supported in React Native, dropped.`);
648
+ break;
649
+ default:
650
+ warn(`Transform fn [${key}] is not supported in React Native, dropped.`);
651
+ }
652
+ });
653
+ return transform;
654
+ }
655
+ // format style transform
656
+ function transformTransform(style) {
657
+ if (!style.transform || Array.isArray(style.transform))
658
+ return;
659
+ style.transform = parseTransform(style.transform);
660
+ }
661
+ function transformBoxShadow(styleObj) {
662
+ if (!styleObj.boxShadow)
663
+ return;
664
+ styleObj.boxShadow = parseValues(styleObj.boxShadow).reduce((res, i, idx) => {
665
+ let formatted;
666
+ // 需要保留 px 关键字,这里仅处理 rpx 转 px
667
+ if (rpxSuffixRegExp.test(i)) {
668
+ formatted = global.__formatValue(i) + 'px';
669
+ }
670
+ else {
671
+ formatted = i;
672
+ }
673
+ return `${res}${idx === 0 ? '' : ' '}${formatted}`;
674
+ }, '');
675
+ }
676
+ // --- shorthand ---
677
+ function expandCompositeValues(values) {
678
+ const v = values.slice(0, 4);
679
+ switch (v.length) {
680
+ case 1:
681
+ return [v[0], v[0], v[0], v[0]];
682
+ case 2:
683
+ return [v[0], v[1], v[0], v[1]];
684
+ case 3:
685
+ return [v[0], v[1], v[2], v[1]];
686
+ default:
687
+ return v;
688
+ }
689
+ }
690
+ function expandAbbreviation(values, props) {
691
+ const result = [];
692
+ for (let i = 0; i < props.length && i < values.length; i++) {
693
+ pushExpandedPair(result, props[i], global.__formatValue(values[i]));
694
+ }
695
+ return result;
696
+ }
697
+ function pushExpandedPair(result, prop, value) {
698
+ if (prop.includes('.')) {
699
+ const [main, sub] = prop.split('.');
700
+ let entry = result.find(item => item[0] === main);
701
+ if (!entry) {
702
+ entry = [main, {}];
703
+ result.push(entry);
704
+ }
705
+ entry[1][sub] = value;
706
+ }
707
+ else {
708
+ result.push([prop, value]);
709
+ }
710
+ }
711
+ function getUnorderedShorthandProp(key, token, used) {
712
+ const props = runtimeAbbreviationMap[key];
713
+ for (const prop of props) {
714
+ if (used[prop])
715
+ continue;
716
+ if (matchRuntimeShorthandProp(prop, token))
717
+ return prop;
718
+ }
719
+ }
720
+ function matchRuntimeShorthandProp(prop, token) {
721
+ if (prop === 'textShadowOffset.width' || prop === 'textShadowOffset.height' || prop === 'textShadowRadius')
722
+ return isLengthValue(token);
723
+ if (prop === 'textShadowColor')
724
+ return isColorValue(token);
725
+ if (prop.endsWith('Width'))
726
+ return isLengthValue(token);
727
+ if (prop === 'textDecorationStyle')
728
+ return hasOwn(textDecorationStyleMap, token);
729
+ if (prop.endsWith('Style'))
730
+ return hasOwn(borderStyleMap, token);
731
+ if (prop.endsWith('Color'))
732
+ return isColorValue(token);
733
+ if (prop === 'flexDirection')
734
+ return hasOwn(flexDirectionMap, token);
735
+ if (prop === 'flexWrap')
736
+ return hasOwn(flexWrapMap, token);
737
+ return false;
738
+ }
739
+ // 通用补齐:扫描完所有 token 后,将 runtimeShorthandDefaultMap 中未被占用(不在 used)的槽位追加到 pairs
740
+ // used 即主循环的占用记录,key 是完整目标 prop 名(含 textShadowOffset.width 这类 dot 路径)
741
+ function applyRuntimeShorthandDefaults(key, pairs, used) {
742
+ const defaults = runtimeShorthandDefaultMap[key];
743
+ if (!defaults)
744
+ return pairs;
745
+ for (const target in defaults) {
746
+ if (!used[target]) {
747
+ pushExpandedPair(pairs, target, defaults[target]);
748
+ }
749
+ }
750
+ return pairs;
751
+ }
752
+ function expandUnorderedAbbreviation(key, values) {
753
+ const result = [];
754
+ const used = {};
755
+ let hasTextDecorationNone = false;
756
+ let hasUnderline = false;
757
+ let hasLineThrough = false;
758
+ for (const value of values) {
759
+ if (key === 'textDecoration' && hasOwn(textDecorationLineMap, value)) {
760
+ switch (value) {
761
+ case 'underline':
762
+ hasUnderline = true;
763
+ continue;
764
+ case 'line-through':
765
+ hasLineThrough = true;
766
+ continue;
767
+ case 'none':
768
+ hasTextDecorationNone = true;
769
+ continue;
770
+ // textDecorationLineMap 命中但分支未处理的 line token:落到通用 getUnorderedShorthandProp 流程
771
+ }
772
+ }
773
+ const prop = getUnorderedShorthandProp(key, value, used);
774
+ if (!prop) {
775
+ // 该 token 与 key 下任何空闲槽位都不匹配(未知类型 / 槽位已占满),静默丢弃可能让用户难以察觉
776
+ warn(`Token [${value}] in [${key}: ${values.join(' ')}] is not a valid value or has no available slot, dropped.`);
777
+ continue;
778
+ }
779
+ used[prop] = true;
780
+ pushExpandedPair(result, prop, global.__formatValue(value));
781
+ }
782
+ if (hasUnderline || hasLineThrough) {
783
+ result.push(['textDecorationLine', hasUnderline && hasLineThrough ? 'underline line-through' : hasUnderline ? 'underline' : 'line-through']);
784
+ }
785
+ else if (hasTextDecorationNone) {
786
+ result.push(['textDecorationLine', 'none']);
787
+ }
788
+ // text-shadow 至少需要 offset-x / offset-y;缺省 height 时按 CSS 默认补 0 并发出 warn
789
+ if (key === 'textShadow') {
790
+ const offsetEntry = result.find(item => item[0] === 'textShadowOffset')?.[1];
791
+ if (offsetEntry && offsetEntry.width !== undefined && offsetEntry.height === undefined) {
792
+ warn(`Value of [textShadow:${values.join(' ')}] is missing offset-y, fallback to 0, please check again!`);
793
+ offsetEntry.height = 0;
794
+ }
795
+ }
796
+ return applyRuntimeShorthandDefaults(key, result, used);
797
+ }
798
+ function expandFlex(value) {
799
+ const values = parseValues(value);
800
+ if (values.length === 0)
801
+ return null;
802
+ if (values.length === 1) {
803
+ if (values[0] === 'none') {
804
+ return [['flexGrow', 0], ['flexShrink', 0]];
805
+ }
806
+ if (values[0] === 'initial') {
807
+ return [['flexGrow', 0], ['flexShrink', 1]];
808
+ }
809
+ }
810
+ const result = [];
811
+ let i = 0;
812
+ const isNum = (v) => !isNaN(+v);
813
+ if (isNum(values[i])) {
814
+ result.push(['flexGrow', +values[i++]]);
815
+ }
816
+ else {
817
+ result.push(['flexGrow', 1]);
818
+ }
819
+ if (i < values.length && isNum(values[i])) {
820
+ result.push(['flexShrink', +values[i++]]);
821
+ }
822
+ else {
823
+ result.push(['flexShrink', 1]);
824
+ }
825
+ if (i < values.length) {
826
+ if (values[i] !== 'auto') {
827
+ result.push(['flexBasis', global.__formatValue(values[i])]);
828
+ }
829
+ }
830
+ else {
831
+ result.push(['flexBasis', 0]);
832
+ }
833
+ return result;
834
+ }
835
+ // font 简写专用 transform,仿 transformFlex 的标志位模式。
836
+ // RN 等效子集语法:font: [ <font-style> ] [ <font-variant-css2> ] [ <font-weight> ] <font-size> [ / <line-height> ] <font-family>
837
+ // - 必填项:font-size 与 font-family;缺其一整条 font 声明丢弃(error)
838
+ // - 非必填 token(font-stretch / 数字型 font-variant-numeric / system 关键字等):warn 提示并忽略,保留其余槽位
839
+ function transformFont(styleObj) {
840
+ const value = styleObj.font;
841
+ if (typeof value !== 'string')
842
+ return;
843
+ const tokens = parseValues(value);
844
+ let sizeIdx = -1;
845
+ let lineHeight;
846
+ const result = [];
847
+ // 1. 定位 font-size(第一个 length,可能带 /<line-height>)
848
+ // 注意:unit-less 数字也命中 length 正则,需要先排除 font-weight 数字(100..900 / bold / normal),
849
+ // 否则 `font: 500 16px Arial` 会把 500 误判为 fontSize。
850
+ // fontSize 自身可能是 %(如 `font: 50% Arial`),保留给文本透传阶段按继承 fontSize 解析;
851
+ // fontSize % 校验通过的是 `isLengthValue`(length 正则含 %),主流程已用 length 分支接住,无需特判
852
+ for (let i = 0; i < tokens.length; i++) {
853
+ let t = tokens[i];
854
+ if (t.endsWith('/') && tokens[i + 1]) {
855
+ t += tokens[i + 1];
856
+ tokens.splice(i + 1, 1);
857
+ }
858
+ else if (tokens[i + 1] === '/' && tokens[i + 2]) {
859
+ t += `/${tokens[i + 2]}`;
860
+ tokens.splice(i + 1, 2);
861
+ }
862
+ else if (tokens[i + 1]?.startsWith('/') && tokens[i + 1].length > 1) {
863
+ t += tokens[i + 1];
864
+ tokens.splice(i + 1, 1);
865
+ }
866
+ const [sizePart, lhPart] = parseValues(t, '/');
867
+ if (hasOwn(fontWeightMap, sizePart))
868
+ continue;
869
+ if (isLengthValue(sizePart)) {
870
+ sizeIdx = i;
871
+ result.push(['fontSize', global.__formatValue(sizePart)]);
872
+ if (lhPart)
873
+ lineHeight = lhPart;
874
+ break;
875
+ }
876
+ }
877
+ if (sizeIdx === -1) {
878
+ // 缺必填 font-size 整体丢弃;与 transformFlex「Flex shorthand value [...] ..., dropped.」同口径
879
+ error(`Font shorthand value [${value}] is missing required <font-size>, dropped.`);
880
+ delete styleObj.font;
881
+ return;
882
+ }
883
+ // 2. 前导段 font-style / font-variant(small-caps) / font-weight,顺序不敏感
884
+ for (let i = 0; i < sizeIdx; i++) {
885
+ const t = tokens[i];
886
+ if (t === 'normal')
887
+ continue;
888
+ if (t === 'italic') {
889
+ result.push(['fontStyle', t]);
890
+ }
891
+ else if (t === 'small-caps') {
892
+ // RN processFontVariant 接受字符串,内部 split 归一为数组,与 font-variant 长属性同口径
893
+ result.push(['fontVariant', t]);
894
+ }
895
+ else if (hasOwn(fontWeightMap, t)) {
896
+ result.push(['fontWeight', t]);
897
+ }
898
+ else {
899
+ // 其余(font-stretch / 数字型 font-variant-numeric / system 关键字等)→ 非必填:
900
+ // 与 transformBackground「Token [...] in [background: ...] ..., dropped.」同口径,warn + 忽略该 token、保留其余
901
+ warn(`Token [${t}] in [font: ${value}] is not supported (only font-style / small-caps / font-weight are valid before <font-size>), dropped.`);
902
+ }
903
+ }
904
+ // 3. line-height:文本透传阶段会基于当前 / 继承 fontSize 解析百分比。
905
+ // - unit-less 数字(1.5)→ '150%' 中间态,避免和已格式化 RN 绝对 number 混淆
906
+ // - 其它值(120% / 40px / 40rpx)→ __formatValue
907
+ if (lineHeight !== undefined) {
908
+ const lh = !isNaN(+lineHeight)
909
+ ? `${+lineHeight * 100}%`
910
+ : global.__formatValue(lineHeight);
911
+ if (lh !== undefined) {
912
+ result.push(['lineHeight', lh]);
913
+ }
914
+ else {
915
+ warn(`Line-height [${lineHeight}] in [font: ${value}] could not be resolved, dropped.`);
916
+ }
917
+ }
918
+ // 4. font-family(font-size 之后剩余部分;多字体取首值、去引号)
919
+ const familyStr = tokens.slice(sizeIdx + 1).join(' ').trim();
920
+ if (!familyStr) {
921
+ error(`Font shorthand value [${value}] is missing required <font-family>, dropped.`);
922
+ delete styleObj.font;
923
+ return;
924
+ }
925
+ // parseValues 内部已 trim
926
+ const family = parseValues(familyStr.replace(quoteCharRegExp, ''), ',')[0];
927
+ if (family)
928
+ result.push(['fontFamily', family]);
929
+ delete styleObj.font;
930
+ for (const [prop, val] of result) {
931
+ if (!hasOwn(styleObj, prop))
932
+ styleObj[prop] = val;
933
+ }
934
+ }
935
+ function transformFlex(styleObj) {
936
+ const value = styleObj.flex;
937
+ if (typeof value !== 'string')
938
+ return;
939
+ delete styleObj.flex;
940
+ const flexResult = expandFlex(value);
941
+ if (!flexResult) {
942
+ // expandFlex 仅在 parseValues 出空数组时返回 null(空字符串 / 纯空白),属于明确非法,整段丢弃
943
+ error(`Flex shorthand value [${value}] is not a valid CSS flex value, dropped.`);
944
+ return;
945
+ }
946
+ for (const [prop, val] of flexResult) {
947
+ if (!hasOwn(styleObj, prop))
948
+ styleObj[prop] = val;
949
+ }
950
+ }
951
+ export function transformShorthand(styleObj, shorthandKeys, percentConfig) {
952
+ for (const key of shorthandKeys) {
953
+ const value = styleObj[key];
954
+ // —— 以下为公共链路(与 textDecoration / flexFlow / textShadow / 四值简写共用)——
955
+ let values;
956
+ if (typeof value === 'string') {
957
+ values = parseValues(value);
958
+ }
959
+ else if (typeof value === 'number' && hasOwn(runtimeNumericExpandShorthandMap, key)) {
960
+ // runtimeNumericExpandShorthandMap 中的简写(border / outline / gap)在 __getStyle 的 transformStyleObj
961
+ // 阶段已被 __formatValue 把带单位长度换算为 number;这里转回字符串 token 交给后续展开,
962
+ // 否则会原样残留为 RN 不认识或行为不一致的简写 key。
963
+ values = ['' + value];
964
+ }
965
+ else {
966
+ continue;
967
+ }
968
+ const props = runtimeAbbreviationMap[key];
969
+ if (!props)
970
+ continue;
971
+ // 单值短路:composite 且单值通常透传(RN 原生支持 margin / padding / inset / border-* 单值 DimensionValue);
972
+ // 但 gap 必须展开 —— RN gap / rowGap / columnGap 严格要求 number,单值串虽已在 __getStyle 阶段被 __formatValue 换算,
973
+ // 多值串(如 '10px 20px')仍会原样透传到这里,需展开后逐 token 再过 __formatValue 才能落成 number;
974
+ // `%` 在写回阶段由 transformShorthand 调 resolvePercent 落成 number(rowGap 基 parentHeight、columnGap 基 parentWidth)
975
+ if (hasOwn(runtimeCompositeStyleMap, key) &&
976
+ values.length === 1 &&
977
+ key !== 'gap')
978
+ continue;
979
+ let expandedValues = values;
980
+ let pairs;
981
+ if (hasOwn(runtimeCompositeStyleMap, key)) {
982
+ expandedValues = expandCompositeValues(values);
983
+ }
984
+ if (hasOwn(runtimeUnorderedAbbreviationMap, key)) {
985
+ // expandUnorderedAbbreviation 末尾已内部调用 applyRuntimeShorthandDefaults 补齐
986
+ pairs = expandUnorderedAbbreviation(key, values);
987
+ }
988
+ else {
989
+ pairs = expandAbbreviation(expandedValues, props);
990
+ }
991
+ delete styleObj[key];
992
+ for (const [prop, val] of pairs) {
993
+ if (hasOwn(styleObj, prop))
994
+ continue;
995
+ // gap 简写展开后的 rowGap / columnGap 仍可能是 `%` 字符串(如 `gap: 50%` / `gap: 10px 50%`),
996
+ // RN 类型严格要求 number,这里就地用 resolvePercent 落成数字(rowGap 基 parentHeight、columnGap 基 parentWidth)
997
+ if ((prop === 'rowGap' || prop === 'columnGap') && typeof val === 'string' && percentRegExp.test(val)) {
998
+ styleObj[prop] = resolvePercent(val, prop, percentConfig);
999
+ }
1000
+ else {
1001
+ styleObj[prop] = val;
1002
+ }
1003
+ }
1004
+ }
1005
+ }
1006
+ // --- runtime alignment ---
1007
+ function transformFontFamily(styleObj) {
1008
+ const value = styleObj.fontFamily;
1009
+ if (typeof value !== 'string')
1010
+ return;
1011
+ const stripped = value.replace(quoteCharRegExp, '').trim();
1012
+ if (!stripped)
1013
+ return;
1014
+ const values = parseValues(stripped, ',');
1015
+ // parseValues 内部已 trim
1016
+ styleObj.fontFamily = values[0];
1017
+ }
1018
+ function transformBorderStyleNone(styleObj) {
1019
+ if (styleObj.borderStyle === 'none') {
1020
+ delete styleObj.borderStyle;
1021
+ delete styleObj.borderTopWidth;
1022
+ delete styleObj.borderRightWidth;
1023
+ delete styleObj.borderBottomWidth;
1024
+ delete styleObj.borderLeftWidth;
1025
+ styleObj.borderWidth = 0;
1026
+ }
1027
+ if (styleObj.outlineStyle === 'none') {
1028
+ delete styleObj.outlineStyle;
1029
+ styleObj.outlineWidth = 0;
1030
+ }
1031
+ }
1032
+ function transformPosition(styleObj) {
1033
+ if (styleObj.position === 'fixed') {
1034
+ styleObj.position = 'absolute';
1035
+ return true;
1036
+ }
1037
+ return false;
1038
+ }
1039
+ function transOrderXY(values) {
1040
+ if (values.length === 2 && ['top', 'bottom'].includes(values[0])) {
1041
+ [values[0], values[1]] = [values[1], values[0]];
1042
+ }
1043
+ return values;
1044
+ }
1045
+ function transformBackground(styleObj) {
1046
+ if (typeof styleObj.backgroundSize === 'string') {
1047
+ styleObj.backgroundSize = parseValues(styleObj.backgroundSize);
1048
+ }
1049
+ if (typeof styleObj.backgroundPosition === 'string') {
1050
+ const parts = parseValues(styleObj.backgroundPosition);
1051
+ styleObj.backgroundPosition = transOrderXY(parts.map(v => v === 'center' ? '50%' : v));
1052
+ }
1053
+ const value = styleObj.background;
1054
+ if (typeof value !== 'string')
1055
+ return;
1056
+ delete styleObj.background;
1057
+ if (value === 'none') {
1058
+ styleObj.backgroundImage = 'none';
1059
+ styleObj.backgroundColor = 'transparent';
1060
+ return;
1061
+ }
1062
+ const tokens = parseValues(value);
1063
+ const positionValues = [];
1064
+ const sizeValues = [];
1065
+ let isSize = false;
1066
+ for (const token of tokens) {
1067
+ if (urlRegExp.test(token)) {
1068
+ styleObj.backgroundImage = token;
1069
+ }
1070
+ else if (linearGradientRegExp.test(token)) {
1071
+ styleObj.backgroundImage = token;
1072
+ }
1073
+ else if (hasOwn(bgRepeatMap, token)) {
1074
+ styleObj.backgroundRepeat = token;
1075
+ }
1076
+ else if (isColorValue(token)) {
1077
+ styleObj.backgroundColor = token;
1078
+ }
1079
+ else if (token === '/') {
1080
+ isSize = true;
1081
+ }
1082
+ else {
1083
+ const slashParts = parseValues(token, '/');
1084
+ if (slashParts.length > 1) {
1085
+ const posPart = slashParts[0];
1086
+ if (posPart)
1087
+ positionValues.push(posPart === 'center' ? '50%' : posPart);
1088
+ isSize = true;
1089
+ for (let i = 1; i < slashParts.length; i++) {
1090
+ if (slashParts[i])
1091
+ sizeValues.push(slashParts[i]);
1092
+ }
1093
+ }
1094
+ else if (isSize) {
1095
+ sizeValues.push(token);
1096
+ }
1097
+ else if (hasOwn(bgPositionMap, token) || percentRegExp.test(token) || digitStartRegExp.test(token)) {
1098
+ positionValues.push(token === 'center' ? '50%' : token);
1099
+ }
1100
+ else {
1101
+ // background-attachment / background-origin / background-clip 及拼写错误等:RN 均不支持,提示用户避免静默丢值
1102
+ warn(`Token [${token}] in [background: ${value}] is not a recognized background sub-value, dropped.`);
1103
+ }
1104
+ }
1105
+ }
1106
+ if (positionValues.length)
1107
+ styleObj.backgroundPosition = transOrderXY(positionValues);
1108
+ if (sizeValues.length)
1109
+ styleObj.backgroundSize = sizeValues;
1110
+ }
1111
+ // ============================================================
1112
+ // style traversal
1113
+ // ============================================================
1114
+ export function traverseStyle(styleObj, visitor) {
1115
+ const keyPath = [];
1116
+ function traverse(target) {
1117
+ if (Array.isArray(target)) {
1118
+ for (let i = 0; i < target.length; i++) {
1119
+ const key = String(i);
1120
+ const value = target[i];
1121
+ keyPath.push(key);
1122
+ visitor({ target, key, value, keyPath });
1123
+ traverse(value);
1124
+ keyPath.pop();
1125
+ }
1126
+ }
1127
+ else if (isObject(target)) {
1128
+ const keys = Object.keys(target);
1129
+ for (let i = 0; i < keys.length; i++) {
1130
+ const key = keys[i];
1131
+ const value = target[key];
1132
+ keyPath.push(key);
1133
+ visitor({ target, key, value, keyPath });
1134
+ traverse(value);
1135
+ keyPath.pop();
381
1136
  }
382
1137
  }
383
- });
384
- return transform;
385
- }
386
- // format style transform
387
- function transformTransform(style) {
388
- if (!style.transform || Array.isArray(style.transform))
389
- return;
390
- style.transform = parseTransform(style.transform);
1138
+ }
1139
+ traverse(styleObj);
391
1140
  }
392
- function transformBoxShadow(styleObj) {
393
- if (!styleObj.boxShadow)
394
- return;
395
- styleObj.boxShadow = parseValues(styleObj.boxShadow).reduce((res, i, idx) => {
396
- return `${res}${idx === 0 ? '' : ' '}${global.__formatValue(i)}`;
397
- }, '');
1141
+ export function setStyle(styleObj, keyPath, setter) {
1142
+ let target = styleObj;
1143
+ const lastKey = keyPath[keyPath.length - 1];
1144
+ for (let i = 0; i < keyPath.length - 1; i++) {
1145
+ target = target[keyPath[i]];
1146
+ if (!target)
1147
+ return;
1148
+ }
1149
+ setter({
1150
+ target,
1151
+ key: lastKey,
1152
+ value: target[lastKey],
1153
+ keyPath
1154
+ });
398
1155
  }
399
- export function useTransformStyle(styleObj = {}, { enableVar, transformRadiusPercent, externalVarContext, parentFontSize, parentWidth, parentHeight }) {
1156
+ // ============================================================
1157
+ // core style hook
1158
+ // ============================================================
1159
+ export function useTransformStyle(styleObj = {}, { enableVar, transformRadiusPercent, parentWidth, parentHeight, defaultStyle }) {
400
1160
  const varStyle = {};
401
1161
  const unoVarStyle = {};
402
1162
  const normalStyle = {};
@@ -404,15 +1164,84 @@ export function useTransformStyle(styleObj = {}, { enableVar, transformRadiusPer
404
1164
  let hasVarUse = false;
405
1165
  let hasSelfPercent = false;
406
1166
  let hasBoxSizingAffectingStyle = false;
1167
+ // 顶层 transform* 标志位:在 styleVisitor 阶段从原始 styleObj 收集,
1168
+ // 调用侧据此决定是否进入对应 transform 函数。
1169
+ let hasTransform = false;
1170
+ let hasBoxShadow = false;
1171
+ let hasFontFamily = false;
1172
+ let hasFlex = false;
1173
+ let hasFont = false;
1174
+ let needTransformBackground = false;
1175
+ let needStringify = false;
407
1176
  const varKeyPaths = [];
408
1177
  const unoVarKeyPaths = [];
409
1178
  const percentKeyPaths = [];
410
1179
  const calcKeyPaths = [];
411
1180
  const envKeyPaths = [];
1181
+ const shorthandKeys = [];
412
1182
  const [width, setWidth] = useState(0);
413
1183
  const [height, setHeight] = useState(0);
414
1184
  const navigation = useNavigation();
415
- function varVisitor({ target, key, value, keyPath }) {
1185
+ function collectTopLevelFlags(key, value) {
1186
+ switch (key) {
1187
+ case 'transform':
1188
+ hasTransform = true;
1189
+ break;
1190
+ case 'boxShadow':
1191
+ hasBoxShadow = true;
1192
+ break;
1193
+ case 'fontFamily':
1194
+ hasFontFamily = true;
1195
+ break;
1196
+ case 'flex':
1197
+ hasFlex = true;
1198
+ break;
1199
+ case 'font':
1200
+ hasFont = true;
1201
+ break;
1202
+ case 'background':
1203
+ case 'backgroundSize':
1204
+ case 'backgroundPosition':
1205
+ needTransformBackground = true;
1206
+ break;
1207
+ case 'fontWeight':
1208
+ case 'transformOrigin':
1209
+ needStringify = true;
1210
+ break;
1211
+ }
1212
+ }
1213
+ function visitOther({ key, value, keyPath }) {
1214
+ if (typeof value !== 'string')
1215
+ return;
1216
+ const hasPercent = value.includes('%');
1217
+ const hasCalc = value.includes('calc(');
1218
+ const hasEnv = value.includes('env(');
1219
+ if (!(hasPercent || hasCalc || hasEnv))
1220
+ return;
1221
+ let resolvedKeyPath;
1222
+ if (hasEnv) {
1223
+ resolvedKeyPath = keyPath.slice();
1224
+ envKeyPaths.push(resolvedKeyPath);
1225
+ }
1226
+ if (hasPercent) {
1227
+ // fixme 去掉 translate & border-radius 的百分比计算
1228
+ // fixme Image 组件 borderRadius 仅支持 number
1229
+ const needRadiusPercent = transformRadiusPercent && hasOwn(radiusPercentRule, key);
1230
+ // RN gap / rowGap / columnGap 不支持 %,需要运行时换算为 number 喂给 RN
1231
+ const needGapPercent = key === 'rowGap' || key === 'columnGap';
1232
+ if ((needRadiusPercent || needGapPercent) && percentRegExp.test(value)) {
1233
+ if (needRadiusPercent)
1234
+ hasSelfPercent = true;
1235
+ resolvedKeyPath = resolvedKeyPath ?? keyPath.slice();
1236
+ percentKeyPaths.push(resolvedKeyPath);
1237
+ }
1238
+ }
1239
+ if (hasCalc) {
1240
+ resolvedKeyPath = resolvedKeyPath ?? keyPath.slice();
1241
+ calcKeyPaths.push(resolvedKeyPath);
1242
+ }
1243
+ }
1244
+ function styleVisitor({ target, key, value, keyPath }) {
416
1245
  if (keyPath.length === 1) {
417
1246
  if (unoVarDecRegExp.test(key)) {
418
1247
  unoVarStyle[key] = value;
@@ -424,57 +1253,32 @@ export function useTransformStyle(styleObj = {}, { enableVar, transformRadiusPer
424
1253
  else {
425
1254
  // clone对象避免set值时改写到props
426
1255
  normalStyle[key] = isObject(value) ? diffAndCloneA(value).clone : value;
1256
+ if (!hasBoxSizingAffectingStyle && isBoxSizingAffectingStyle(key)) {
1257
+ hasBoxSizingAffectingStyle = true;
1258
+ }
1259
+ if (hasOwn(runtimeAbbreviationMap, key)) {
1260
+ shorthandKeys.push(key);
1261
+ }
1262
+ collectTopLevelFlags(key, value);
427
1263
  }
428
1264
  }
429
- // 对于var定义中使用的var无需替换值,可以通过resolveVar递归解析出值
430
- if (!varDecRegExp.test(key)) {
431
- // 一般情况下一个样式属性中不会混用unocss var和普通css var,可分开进行互斥处理
432
- if (unoVarUseRegExp.test(value)) {
433
- unoVarKeyPaths.push(keyPath.slice());
434
- }
435
- else if (varUseRegExp.test(value)) {
436
- hasVarUse = true;
437
- varKeyPaths.push(keyPath.slice());
438
- }
439
- else {
440
- visitOther({ target, key, value, keyPath });
441
- }
442
- }
443
- }
444
- function boxSizingVisitor({ key, keyPath }) {
445
- if (keyPath.length === 1 && !hasBoxSizingAffectingStyle && isBoxSizingAffectingStyle(key)) {
446
- hasBoxSizingAffectingStyle = true;
447
- }
448
- }
449
- function envVisitor({ value, keyPath }) {
450
- if (envUseRegExp.test(value)) {
451
- envKeyPaths.push(keyPath.slice());
452
- }
453
- }
454
- function calcVisitor({ value, keyPath }) {
455
- if (calcUseRegExp.test(value)) {
456
- calcKeyPaths.push(keyPath.slice());
457
- }
458
- }
459
- function percentVisitor({ key, value, keyPath }) {
460
- // fixme 去掉 translate & border-radius 的百分比计算
461
- // fixme Image 组件 borderRadius 仅支持 number
462
- if (transformRadiusPercent && hasOwn(radiusPercentRule, key) && PERCENT_REGEX.test(value)) {
463
- hasSelfPercent = true;
464
- percentKeyPaths.push(keyPath.slice());
1265
+ // var 定义中使用的 var 无需替换值,可以通过 resolveVar 递归解析出值
1266
+ if (varDecRegExp.test(key) || typeof value !== 'string')
1267
+ return;
1268
+ // 一般情况下一个样式属性中不会混用 unocss var 和普通 css var,可分开互斥处理
1269
+ if (unoVarUseRegExp.test(value)) {
1270
+ unoVarKeyPaths.push(keyPath.slice());
465
1271
  }
466
- else if ((key === 'fontSize' || key === 'lineHeight') && PERCENT_REGEX.test(value)) {
467
- percentKeyPaths.push(keyPath.slice());
1272
+ else if (varUseRegExp.test(value)) {
1273
+ hasVarUse = true;
1274
+ varKeyPaths.push(keyPath.slice());
468
1275
  }
469
- }
470
- function visitOther({ target, key, value, keyPath }) {
471
- if (typeof value === 'string' && (value.includes('%') || value.includes('calc(') || value.includes('env('))) {
472
- [envVisitor, percentVisitor, calcVisitor].forEach(visitor => visitor({ target, key, value, keyPath }));
1276
+ else {
1277
+ visitOther({ target, key, value, keyPath });
473
1278
  }
474
1279
  }
475
1280
  // traverse var & generate normalStyle
476
- traverseStyle(styleObj, [varVisitor, boxSizingVisitor]);
477
- hasVarDec = hasVarDec || !!externalVarContext;
1281
+ traverseStyle(styleObj, styleVisitor);
478
1282
  enableVar = enableVar || hasVarDec || hasVarUse;
479
1283
  const enableVarRef = useRef(enableVar);
480
1284
  if (enableVarRef.current !== enableVar) {
@@ -485,61 +1289,97 @@ export function useTransformStyle(styleObj = {}, { enableVar, transformRadiusPer
485
1289
  if (enableVarRef.current) {
486
1290
  // eslint-disable-next-line react-hooks/rules-of-hooks
487
1291
  const varContext = useContext(VarContext);
488
- const newVarContext = extendObject({}, varContext, externalVarContext, varStyle);
489
- // 缓存比较newVarContext是否发生变化
490
- if (diffAndCloneA(varContextRef.current, newVarContext).diff) {
491
- varContextRef.current = newVarContext;
1292
+ // 无声明节点:直接用父 varContext 解析,跳过 merge / 比对 / ref 维护。
1293
+ // 有声明节点:浅合并出 newVarContext,仅在内容变化时替换 ref 维持引用稳定,
1294
+ // 供 wrapChildren 的 <VarContext.Provider> 使用。
1295
+ let resolvedVarContext = varContext;
1296
+ if (hasVarDec) {
1297
+ const newVarContext = extendObject({}, varContext, varStyle);
1298
+ if (!shallowEqual(varContextRef.current, newVarContext)) {
1299
+ varContextRef.current = newVarContext;
1300
+ }
1301
+ resolvedVarContext = varContextRef.current;
492
1302
  }
493
- transformVar(normalStyle, varKeyPaths, varContextRef.current, visitOther);
1303
+ if (varKeyPaths.length)
1304
+ transformVar(normalStyle, varKeyPaths, resolvedVarContext, visitOther);
494
1305
  }
495
1306
  // apply unocss var
496
- if (unoVarKeyPaths.length) {
1307
+ if (unoVarKeyPaths.length)
497
1308
  transformVar(normalStyle, unoVarKeyPaths, unoVarStyle, visitOther);
498
- }
1309
+ // apply env
1310
+ if (envKeyPaths.length)
1311
+ transformEnv(normalStyle, envKeyPaths, navigation);
1312
+ // apply percent / calc
499
1313
  const percentConfig = {
500
1314
  width,
501
1315
  height,
502
1316
  fontSize: normalStyle.fontSize,
503
1317
  parentWidth,
504
- parentHeight,
505
- parentFontSize
1318
+ parentHeight
506
1319
  };
507
- const positionMeta = {
508
- hasPositionFixed: false
509
- };
510
- // apply env
511
- transformEnv(normalStyle, envKeyPaths, navigation);
512
- // apply percent
513
- transformPercent(normalStyle, percentKeyPaths, percentConfig);
514
- // apply calc
515
- transformCalc(normalStyle, calcKeyPaths, (value, key) => {
516
- if (PERCENT_REGEX.test(value)) {
517
- if (hasOwn(selfPercentRule, key)) {
518
- hasSelfPercent = true;
519
- }
520
- const resolved = resolvePercent(value, key, percentConfig);
521
- return typeof resolved === 'number' ? resolved : 0;
522
- }
523
- else {
524
- const formatted = global.__formatValue(value);
525
- if (typeof formatted === 'number') {
526
- return formatted;
1320
+ if (hasFont)
1321
+ transformFont(normalStyle);
1322
+ if (percentKeyPaths.length)
1323
+ transformPercent(normalStyle, percentKeyPaths, percentConfig);
1324
+ if (calcKeyPaths.length) {
1325
+ transformCalc(normalStyle, calcKeyPaths, (value, key) => {
1326
+ if (percentRegExp.test(value)) {
1327
+ if (hasOwn(selfPercentRule, key)) {
1328
+ hasSelfPercent = true;
1329
+ }
1330
+ const resolved = resolvePercent(value, key, percentConfig);
1331
+ return typeof resolved === 'number' ? resolved : 0;
527
1332
  }
528
1333
  else {
529
- warn('calc() only support number, px, rpx, % temporarily.');
530
- return 0;
1334
+ const formatted = global.__formatValue(value);
1335
+ if (typeof formatted === 'number') {
1336
+ return formatted;
1337
+ }
1338
+ else {
1339
+ warn('calc() only support number, px, rpx, % temporarily.');
1340
+ return 0;
1341
+ }
531
1342
  }
532
- }
533
- });
534
- // apply position
535
- transformPosition(normalStyle, positionMeta);
1343
+ });
1344
+ }
536
1345
  // transform number enum stringify
537
- transformStringify(normalStyle);
538
- // transform rpx to px
539
- transformBoxShadow(normalStyle);
540
- // transform 字符串格式转化数组格式(先转数组再处理css var)
541
- transformTransform(normalStyle);
542
- transformBoxSizing(normalStyle, hasBoxSizingAffectingStyle);
1346
+ if (needStringify)
1347
+ transformStringify(normalStyle);
1348
+ // transform unit
1349
+ if (hasBoxShadow)
1350
+ transformBoxShadow(normalStyle);
1351
+ // transform 字符串格式转化数组格式
1352
+ if (hasTransform)
1353
+ transformTransform(normalStyle);
1354
+ // apply runtime style processing alignment
1355
+ if (hasFontFamily)
1356
+ transformFontFamily(normalStyle);
1357
+ if (hasFlex)
1358
+ transformFlex(normalStyle);
1359
+ if (shorthandKeys.length)
1360
+ transformShorthand(normalStyle, shorthandKeys, percentConfig);
1361
+ if (needTransformBackground)
1362
+ transformBackground(normalStyle);
1363
+ // borderStyle / outlineStyle: 'none' 的清除语义放到变量解析、简写展开之后统一处理。
1364
+ transformBorderStyleNone(normalStyle);
1365
+ // transform position: fixed
1366
+ const hasPositionFixed = transformPosition(normalStyle);
1367
+ // 合并组件默认样式:默认样式在 user transform 之后兜底写入,需完全符合 RN style 规范
1368
+ // 命中 user 已存在 key(含简写展开后的长属性)则跳过。
1369
+ // 复用同一次循环顺带补 hasBoxSizingAffectingStyle,避免独立扫一遍 default。
1370
+ if (defaultStyle) {
1371
+ for (const k in defaultStyle) {
1372
+ if (!hasOwn(defaultStyle, k))
1373
+ continue;
1374
+ if (!hasOwn(normalStyle, k))
1375
+ normalStyle[k] = defaultStyle[k];
1376
+ if (!hasBoxSizingAffectingStyle && isBoxSizingAffectingStyle(k)) {
1377
+ hasBoxSizingAffectingStyle = true;
1378
+ }
1379
+ }
1380
+ }
1381
+ if (hasBoxSizingAffectingStyle)
1382
+ transformBoxSizing(normalStyle);
543
1383
  return {
544
1384
  hasVarDec,
545
1385
  varContextRef,
@@ -547,61 +1387,66 @@ export function useTransformStyle(styleObj = {}, { enableVar, transformRadiusPer
547
1387
  setHeight,
548
1388
  normalStyle,
549
1389
  hasSelfPercent,
550
- hasPositionFixed: positionMeta.hasPositionFixed
1390
+ hasPositionFixed
551
1391
  };
552
1392
  }
553
- export function traverseStyle(styleObj, visitors) {
554
- const keyPath = [];
555
- function traverse(target) {
556
- if (Array.isArray(target)) {
557
- target.forEach((value, index) => {
558
- const key = String(index);
559
- keyPath.push(key);
560
- visitors.forEach(visitor => visitor({ target, key, value, keyPath }));
561
- traverse(value);
562
- keyPath.pop();
563
- });
564
- }
565
- else if (isObject(target)) {
566
- Object.entries(target).forEach(([key, value]) => {
567
- keyPath.push(key);
568
- visitors.forEach(visitor => visitor({ target, key, value, keyPath }));
569
- traverse(value);
570
- keyPath.pop();
571
- });
572
- }
1393
+ // ============================================================
1394
+ // other React hooks
1395
+ // ============================================================
1396
+ export function useNavigation() {
1397
+ const { navigation } = useContext(RouteContext) || {};
1398
+ return navigation;
1399
+ }
1400
+ function getTextPercentBase(currentFontSize, parentTextStyle) {
1401
+ return typeof currentFontSize === 'number'
1402
+ ? currentFontSize
1403
+ : typeof parentTextStyle?.fontSize === 'number'
1404
+ ? parentTextStyle.fontSize
1405
+ : DEFAULT_FONT_SIZE;
1406
+ }
1407
+ export function resolveTextFontSizePercentStyle(textStyle, parentTextStyle) {
1408
+ if (!textStyle)
1409
+ return textStyle;
1410
+ if (typeof textStyle.fontSize === 'string' && percentRegExp.test(textStyle.fontSize)) {
1411
+ const base = getTextPercentBase(undefined, parentTextStyle);
1412
+ textStyle.fontSize = parseFloat(textStyle.fontSize) / 100 * base;
573
1413
  }
574
- traverse(styleObj);
1414
+ return textStyle;
575
1415
  }
576
- export function setStyle(styleObj, keyPath, setter) {
577
- let target = styleObj;
578
- const lastKey = keyPath[keyPath.length - 1];
579
- for (let i = 0; i < keyPath.length - 1; i++) {
580
- target = target[keyPath[i]];
581
- if (!target)
582
- return;
1416
+ export function resolveTextLineHeightPercentStyle(textStyle, parentTextStyle) {
1417
+ if (!textStyle)
1418
+ return textStyle;
1419
+ if (typeof textStyle.lineHeight === 'string' && percentRegExp.test(textStyle.lineHeight)) {
1420
+ const base = getTextPercentBase(textStyle.fontSize, parentTextStyle);
1421
+ textStyle.lineHeight = parseFloat(textStyle.lineHeight) / 100 * base;
583
1422
  }
584
- setter({
585
- target,
586
- key: lastKey,
587
- value: target[lastKey],
588
- keyPath
589
- });
1423
+ return textStyle;
590
1424
  }
591
- export function splitProps(props) {
592
- return groupBy(props, (key) => {
593
- if (hasOwn(TEXT_PROPS_MAP, key)) {
594
- return 'textProps';
1425
+ /**
1426
+ * 用法等同于 useEffect,但是会忽略首次执行,只在依赖更新时执行
1427
+ */
1428
+ export const useUpdateEffect = (effect, deps) => {
1429
+ const isMounted = useRef(false);
1430
+ // for react-refresh
1431
+ useEffect(() => {
1432
+ return () => {
1433
+ isMounted.current = false;
1434
+ };
1435
+ }, []);
1436
+ useEffect(() => {
1437
+ if (!isMounted.current) {
1438
+ isMounted.current = true;
595
1439
  }
596
1440
  else {
597
- return 'innerProps';
1441
+ return effect();
598
1442
  }
599
- });
600
- }
1443
+ }, deps);
1444
+ };
601
1445
  export const useLayout = ({ props, hasSelfPercent, setWidth, setHeight, onLayout, nodeRef }) => {
602
1446
  const layoutRef = useRef({});
603
1447
  const hasLayoutRef = useRef(false);
604
- const layoutStyle = useMemo(() => { return !hasLayoutRef.current && hasSelfPercent ? HIDDEN_STYLE : {}; }, [hasLayoutRef.current]);
1448
+ // 固定首次 layout 前是否需要隐藏,避免 hasSelfPercent 后续变化时重新隐藏组件
1449
+ const layoutStyle = useMemo(() => { return !hasLayoutRef.current && hasSelfPercent ? hiddenStyle : undefined; }, [hasLayoutRef.current]);
605
1450
  const layoutProps = {};
606
1451
  const navigation = useNavigation();
607
1452
  const enableOffset = props['enable-offset'];
@@ -629,21 +1474,27 @@ export const useLayout = ({ props, hasSelfPercent, setWidth, setHeight, onLayout
629
1474
  layoutProps
630
1475
  };
631
1476
  };
632
- export function useTextPassThroughValue(textStyle, textProps, { inheritTextProps = true, disabled = false } = {}) {
1477
+ export function useTextPassThrough(textStyle, textProps, { enableTextPassThrough = false } = {}) {
1478
+ const shouldEnableTextPassThrough = (enableTextPassThrough ||
1479
+ !!textStyle ||
1480
+ !!textProps);
1481
+ const enableTextPassThroughRef = useRef(shouldEnableTextPassThrough);
1482
+ if (enableTextPassThroughRef.current !== shouldEnableTextPassThrough) {
1483
+ error('[Mpx runtime error]: text style/props use should be stable in the component lifecycle, or you can set [enable-text-pass-through] with true.');
1484
+ }
1485
+ if (!enableTextPassThroughRef.current)
1486
+ return null;
1487
+ // eslint-disable-next-line react-hooks/rules-of-hooks
633
1488
  const parent = useContext(TextPassThroughContext);
1489
+ // eslint-disable-next-line react-hooks/rules-of-hooks
634
1490
  const valueRef = useRef(null);
635
- if (disabled)
636
- return null;
637
- if (!textStyle && !textProps && (inheritTextProps || !parent?.pendingTextProps))
638
- return null;
639
- const nextTextStyle = textStyle
640
- ? extendObject({}, parent?.textStyle, textStyle)
1491
+ const resolvedTextStyle = resolveTextFontSizePercentStyle(textStyle, parent?.textStyle);
1492
+ const nextTextStyle = resolvedTextStyle
1493
+ ? extendObject({}, parent?.textStyle, resolvedTextStyle)
641
1494
  : parent?.textStyle;
642
- const nextTextProps = inheritTextProps
643
- ? textProps
644
- ? extendObject({}, parent?.pendingTextProps, textProps)
645
- : parent?.pendingTextProps
646
- : textProps;
1495
+ const nextTextProps = textProps
1496
+ ? extendObject({}, parent?.pendingTextProps, textProps)
1497
+ : parent?.pendingTextProps;
647
1498
  const nextValue = {
648
1499
  textStyle: nextTextStyle,
649
1500
  pendingTextProps: nextTextProps
@@ -653,77 +1504,6 @@ export function useTextPassThroughValue(textStyle, textProps, { inheritTextProps
653
1504
  }
654
1505
  return valueRef.current;
655
1506
  }
656
- export function wrapChildren(props = {}, { hasVarDec, varContext, textPassThrough }) {
657
- let { children } = props;
658
- if (textPassThrough) {
659
- children = <TextPassThroughContext.Provider value={textPassThrough} key='textPassThroughWrap'>{children}</TextPassThroughContext.Provider>;
660
- }
661
- if (hasVarDec && varContext) {
662
- children = <VarContext.Provider value={varContext} key='varContextWrap'>{children}</VarContext.Provider>;
663
- }
664
- return children;
665
- }
666
- export const debounce = (func, delay) => {
667
- let timer;
668
- const wrapper = (...args) => {
669
- timer && clearTimeout(timer);
670
- timer = setTimeout(() => {
671
- func(...args);
672
- }, delay);
673
- };
674
- wrapper.clear = () => {
675
- timer && clearTimeout(timer);
676
- timer = null;
677
- };
678
- return wrapper;
679
- };
680
- export const useDebounceCallback = (func, delay) => {
681
- const debounced = useMemo(() => debounce(func, delay), [func]);
682
- return debounced;
683
- };
684
- export const useStableCallback = (callback) => {
685
- const ref = useRef(callback);
686
- ref.current = callback;
687
- return useCallback((...args) => ref.current?.(...args), []);
688
- };
689
- export function usePrevious(value) {
690
- const ref = useRef();
691
- const prev = ref.current;
692
- ref.current = value;
693
- return prev;
694
- }
695
- export function flatGesture(gestures = []) {
696
- return (gestures && gestures.flatMap((gesture) => {
697
- if (gesture && gesture.nodeRefs) {
698
- return gesture.nodeRefs
699
- .map((item) => item.getNodeInstance()?.instance?.gestureRef || {});
700
- }
701
- return gesture?.current ? [gesture] : [];
702
- })) || [];
703
- }
704
- export function getCurrentPage(pageId) {
705
- if (!global.getCurrentPages)
706
- return;
707
- const pages = global.getCurrentPages();
708
- return pages.find((page) => isFunction(page.getPageId) && page.getPageId() === pageId);
709
- }
710
- export function renderImage(imageProps, enableFastImage = true) {
711
- let Component = Image;
712
- if (enableFastImage) {
713
- // eslint-disable-next-line @typescript-eslint/no-var-requires
714
- const fastImageModule = require('@d11/react-native-fast-image');
715
- Component = fastImageModule.default || fastImageModule;
716
- }
717
- return createElement(Component, imageProps);
718
- }
719
- export function pickStyle(styleObj = {}, pickedKeys, callback) {
720
- return pickedKeys.reduce((acc, key) => {
721
- if (key in styleObj) {
722
- acc[key] = callback ? callback(key, styleObj[key]) : styleObj[key];
723
- }
724
- return acc;
725
- }, {});
726
- }
727
1507
  export function useHover({ enableHover, hoverStartTime, hoverStayTime, disabled }) {
728
1508
  const enableHoverRef = useRef(enableHover);
729
1509
  if (enableHoverRef.current !== enableHover) {
@@ -736,33 +1516,38 @@ export function useHover({ enableHover, hoverStartTime, hoverStayTime, disabled
736
1516
  // eslint-disable-next-line react-hooks/rules-of-hooks
737
1517
  const [isHover, setIsHover] = useState(false);
738
1518
  // eslint-disable-next-line react-hooks/rules-of-hooks
739
- const dataRef = useRef({});
1519
+ const hoverRef = useRef({ hoverStartTime, hoverStayTime, disabled });
1520
+ hoverRef.current.hoverStartTime = hoverStartTime;
1521
+ hoverRef.current.hoverStayTime = hoverStayTime;
1522
+ hoverRef.current.disabled = disabled;
740
1523
  // eslint-disable-next-line react-hooks/rules-of-hooks
741
1524
  useEffect(() => {
742
1525
  return () => {
743
- dataRef.current.startTimer && clearTimeout(dataRef.current.startTimer);
744
- dataRef.current.stayTimer && clearTimeout(dataRef.current.stayTimer);
1526
+ hoverRef.current.startTimer && clearTimeout(hoverRef.current.startTimer);
1527
+ hoverRef.current.stayTimer && clearTimeout(hoverRef.current.stayTimer);
745
1528
  };
746
1529
  }, []);
747
- const setStartTimer = () => {
748
- if (disabled)
749
- return;
750
- dataRef.current.startTimer && clearTimeout(dataRef.current.startTimer);
751
- dataRef.current.startTimer = setTimeout(() => {
752
- setIsHover(true);
753
- }, +hoverStartTime);
754
- };
755
- const setStayTimer = () => {
756
- if (disabled)
757
- return;
758
- dataRef.current.stayTimer && clearTimeout(dataRef.current.stayTimer);
759
- dataRef.current.startTimer && clearTimeout(dataRef.current.startTimer);
760
- dataRef.current.stayTimer = setTimeout(() => {
761
- setIsHover(false);
762
- }, +hoverStayTime);
763
- };
764
1530
  // eslint-disable-next-line react-hooks/rules-of-hooks
765
1531
  const gesture = useMemo(() => {
1532
+ const setStartTimer = () => {
1533
+ const data = hoverRef.current;
1534
+ if (data.disabled)
1535
+ return;
1536
+ data.startTimer && clearTimeout(data.startTimer);
1537
+ data.startTimer = setTimeout(() => {
1538
+ setIsHover(true);
1539
+ }, +data.hoverStartTime);
1540
+ };
1541
+ const setStayTimer = () => {
1542
+ const data = hoverRef.current;
1543
+ if (data.disabled)
1544
+ return;
1545
+ data.stayTimer && clearTimeout(data.stayTimer);
1546
+ data.startTimer && clearTimeout(data.startTimer);
1547
+ data.stayTimer = setTimeout(() => {
1548
+ setIsHover(false);
1549
+ }, +data.hoverStayTime);
1550
+ };
766
1551
  return Gesture.Pan()
767
1552
  .onTouchesDown(() => {
768
1553
  setStartTimer();
@@ -793,3 +1578,57 @@ export function useRunOnJSCallback(callbackMapRef) {
793
1578
  }, []);
794
1579
  return invokeCallback;
795
1580
  }
1581
+ export const useDebounceCallback = (func, delay) => {
1582
+ const debounced = useMemo(() => debounce(func, delay), [func]);
1583
+ return debounced;
1584
+ };
1585
+ export const useStableCallback = (callback) => {
1586
+ const ref = useRef(callback);
1587
+ ref.current = callback;
1588
+ return useCallback((...args) => ref.current?.(...args), []);
1589
+ };
1590
+ export function usePrevious(value) {
1591
+ const ref = useRef();
1592
+ const prev = ref.current;
1593
+ ref.current = value;
1594
+ return prev;
1595
+ }
1596
+ // ============================================================
1597
+ // component helpers
1598
+ // ============================================================
1599
+ export function wrapChildren(children, { hasVarDec, varContext, textPassThrough }) {
1600
+ if (textPassThrough) {
1601
+ children = <TextPassThroughContext.Provider value={textPassThrough} key='textPassThroughWrap'>{children}</TextPassThroughContext.Provider>;
1602
+ }
1603
+ if (hasVarDec && varContext) {
1604
+ children = <VarContext.Provider value={varContext} key='varContextWrap'>{children}</VarContext.Provider>;
1605
+ }
1606
+ return children;
1607
+ }
1608
+ export function renderImage(imageProps, enableFastImage = true) {
1609
+ let Component = Image;
1610
+ if (enableFastImage) {
1611
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1612
+ const fastImageModule = require('@d11/react-native-fast-image');
1613
+ Component = fastImageModule.default || fastImageModule;
1614
+ }
1615
+ return createElement(Component, imageProps);
1616
+ }
1617
+ export function flatGesture(gestures = []) {
1618
+ return (gestures && gestures.flatMap((gesture) => {
1619
+ if (gesture && gesture.nodeRefs) {
1620
+ return gesture.nodeRefs
1621
+ .map((item) => item.getNodeInstance()?.instance?.gestureRef || {});
1622
+ }
1623
+ if (gesture && (gesture.current || gesture.handlerTag !== undefined)) {
1624
+ return [gesture];
1625
+ }
1626
+ return [];
1627
+ })) || [];
1628
+ }
1629
+ export function getCurrentPage(pageId) {
1630
+ if (!global.getCurrentPages)
1631
+ return;
1632
+ const pages = global.getCurrentPages();
1633
+ return pages.find((page) => isFunction(page.getPageId) && page.getPageId() === pageId);
1634
+ }