@unif/react-native-design 0.31.3 → 0.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +23 -171
  2. package/lib/module/components/ui/Avatar/Avatar.js +2 -1
  3. package/lib/module/components/ui/Avatar/Avatar.js.map +1 -1
  4. package/lib/module/components/ui/Avatar/geometry.js +43 -0
  5. package/lib/module/components/ui/Avatar/geometry.js.map +1 -0
  6. package/lib/module/components/ui/Avatar/styles.js +1 -41
  7. package/lib/module/components/ui/Avatar/styles.js.map +1 -1
  8. package/lib/module/components/ui/AvatarGroup/AvatarGroup.js +1 -1
  9. package/lib/module/components/ui/AvatarGroup/AvatarGroup.js.map +1 -1
  10. package/lib/module/components/ui/CircularProgress/CircularProgress.js +14 -25
  11. package/lib/module/components/ui/CircularProgress/CircularProgress.js.map +1 -1
  12. package/lib/module/components/ui/CircularProgress/normalizeCircularProgress.js.map +1 -1
  13. package/lib/module/components/ui/CircularProgress/styles.js +24 -0
  14. package/lib/module/components/ui/CircularProgress/styles.js.map +1 -0
  15. package/lib/module/components/ui/Pulse/PulseDot.js +5 -5
  16. package/lib/module/components/ui/Pulse/PulseDot.js.map +1 -1
  17. package/lib/module/components/ui/Pulse/constants.js +15 -0
  18. package/lib/module/components/ui/Pulse/constants.js.map +1 -0
  19. package/lib/module/components/ui/Pulse/usePulse.js +4 -62
  20. package/lib/module/components/ui/Pulse/usePulse.js.map +1 -1
  21. package/lib/module/components/ui/Skeleton/Skeleton.js +6 -2
  22. package/lib/module/components/ui/Skeleton/Skeleton.js.map +1 -1
  23. package/lib/module/components/ui/Skeleton/constants.js +9 -0
  24. package/lib/module/components/ui/Skeleton/constants.js.map +1 -0
  25. package/lib/module/components/ui/TextField/TextFieldBase.js +21 -4
  26. package/lib/module/components/ui/TextField/TextFieldBase.js.map +1 -1
  27. package/lib/module/components/ui/TextField/constants.js +7 -0
  28. package/lib/module/components/ui/TextField/constants.js.map +1 -0
  29. package/lib/module/components/ui/TextField/styles.js +10 -2
  30. package/lib/module/components/ui/TextField/styles.js.map +1 -1
  31. package/lib/module/components/ui/TextField/useMultilineLayout.js +18 -0
  32. package/lib/module/components/ui/TextField/useMultilineLayout.js.map +1 -0
  33. package/lib/module/components/ui/TextField/useMultilineLayout.web.js +44 -0
  34. package/lib/module/components/ui/TextField/useMultilineLayout.web.js.map +1 -0
  35. package/lib/module/components/ui/Thumbnail/Thumbnail.js +13 -6
  36. package/lib/module/components/ui/Thumbnail/Thumbnail.js.map +1 -1
  37. package/lib/module/components/ui/Thumbnail/constants.js +22 -0
  38. package/lib/module/components/ui/Thumbnail/constants.js.map +1 -0
  39. package/lib/module/components/ui/Thumbnail/normalize.js +30 -1
  40. package/lib/module/components/ui/Thumbnail/normalize.js.map +1 -1
  41. package/lib/module/components/ui/Thumbnail/styles.js +5 -28
  42. package/lib/module/components/ui/Thumbnail/styles.js.map +1 -1
  43. package/lib/module/components/ui/index.js.map +1 -1
  44. package/lib/module/components/ui/shared/pulse/constants.js +5 -0
  45. package/lib/module/components/ui/shared/pulse/constants.js.map +1 -0
  46. package/lib/module/components/ui/{Pulse → shared/pulse}/normalizePulseOptions.js +2 -12
  47. package/lib/module/components/ui/shared/pulse/normalizePulseOptions.js.map +1 -0
  48. package/lib/module/components/ui/shared/pulse/types.js +2 -0
  49. package/lib/module/components/ui/shared/pulse/types.js.map +1 -0
  50. package/lib/module/components/ui/shared/pulse/usePulseAnimation.js +30 -0
  51. package/lib/module/components/ui/shared/pulse/usePulseAnimation.js.map +1 -0
  52. package/lib/module/components/ui/{Pulse → shared/pulse}/usePulseDriver.js +1 -1
  53. package/lib/module/components/ui/shared/pulse/usePulseDriver.js.map +1 -0
  54. package/lib/module/components/ui/{Pulse → shared/pulse}/usePulseDriver.web.js +2 -17
  55. package/lib/module/components/ui/shared/pulse/usePulseDriver.web.js.map +1 -0
  56. package/lib/typescript/src/components/ui/Avatar/Avatar.d.ts.map +1 -1
  57. package/lib/typescript/src/components/ui/Avatar/geometry.d.ts +7 -0
  58. package/lib/typescript/src/components/ui/Avatar/geometry.d.ts.map +1 -0
  59. package/lib/typescript/src/components/ui/Avatar/styles.d.ts +1 -9
  60. package/lib/typescript/src/components/ui/Avatar/styles.d.ts.map +1 -1
  61. package/lib/typescript/src/components/ui/Avatar/types.d.ts +7 -2
  62. package/lib/typescript/src/components/ui/Avatar/types.d.ts.map +1 -1
  63. package/lib/typescript/src/components/ui/CircularProgress/CircularProgress.d.ts.map +1 -1
  64. package/lib/typescript/src/components/ui/CircularProgress/normalizeCircularProgress.d.ts +2 -14
  65. package/lib/typescript/src/components/ui/CircularProgress/normalizeCircularProgress.d.ts.map +1 -1
  66. package/lib/typescript/src/components/ui/CircularProgress/styles.d.ts +19 -0
  67. package/lib/typescript/src/components/ui/CircularProgress/styles.d.ts.map +1 -0
  68. package/lib/typescript/src/components/ui/CircularProgress/types.d.ts +17 -2
  69. package/lib/typescript/src/components/ui/CircularProgress/types.d.ts.map +1 -1
  70. package/lib/typescript/src/components/ui/Pulse/PulseDot.d.ts.map +1 -1
  71. package/lib/typescript/src/components/ui/Pulse/constants.d.ts +4 -0
  72. package/lib/typescript/src/components/ui/Pulse/constants.d.ts.map +1 -0
  73. package/lib/typescript/src/components/ui/Pulse/types.d.ts +6 -6
  74. package/lib/typescript/src/components/ui/Pulse/types.d.ts.map +1 -1
  75. package/lib/typescript/src/components/ui/Pulse/usePulse.d.ts +0 -9
  76. package/lib/typescript/src/components/ui/Pulse/usePulse.d.ts.map +1 -1
  77. package/lib/typescript/src/components/ui/Skeleton/Skeleton.d.ts.map +1 -1
  78. package/lib/typescript/src/components/ui/Skeleton/constants.d.ts +3 -0
  79. package/lib/typescript/src/components/ui/Skeleton/constants.d.ts.map +1 -0
  80. package/lib/typescript/src/components/ui/Skeleton/types.d.ts +2 -2
  81. package/lib/typescript/src/components/ui/Skeleton/types.d.ts.map +1 -1
  82. package/lib/typescript/src/components/ui/TextField/TextFieldBase.d.ts.map +1 -1
  83. package/lib/typescript/src/components/ui/TextField/constants.d.ts +4 -0
  84. package/lib/typescript/src/components/ui/TextField/constants.d.ts.map +1 -0
  85. package/lib/typescript/src/components/ui/TextField/styles.d.ts +6 -0
  86. package/lib/typescript/src/components/ui/TextField/styles.d.ts.map +1 -1
  87. package/lib/typescript/src/components/ui/TextField/types.d.ts +36 -11
  88. package/lib/typescript/src/components/ui/TextField/types.d.ts.map +1 -1
  89. package/lib/typescript/src/components/ui/TextField/useMultilineLayout.d.ts +4 -0
  90. package/lib/typescript/src/components/ui/TextField/useMultilineLayout.d.ts.map +1 -0
  91. package/lib/typescript/src/components/ui/TextField/useMultilineLayout.web.d.ts +4 -0
  92. package/lib/typescript/src/components/ui/TextField/useMultilineLayout.web.d.ts.map +1 -0
  93. package/lib/typescript/src/components/ui/Textarea/types.d.ts +4 -2
  94. package/lib/typescript/src/components/ui/Textarea/types.d.ts.map +1 -1
  95. package/lib/typescript/src/components/ui/Thumbnail/Thumbnail.d.ts.map +1 -1
  96. package/lib/typescript/src/components/ui/Thumbnail/constants.d.ts +4 -0
  97. package/lib/typescript/src/components/ui/Thumbnail/constants.d.ts.map +1 -0
  98. package/lib/typescript/src/components/ui/Thumbnail/index.d.ts +1 -1
  99. package/lib/typescript/src/components/ui/Thumbnail/index.d.ts.map +1 -1
  100. package/lib/typescript/src/components/ui/Thumbnail/normalize.d.ts +2 -6
  101. package/lib/typescript/src/components/ui/Thumbnail/normalize.d.ts.map +1 -1
  102. package/lib/typescript/src/components/ui/Thumbnail/styles.d.ts +9 -8
  103. package/lib/typescript/src/components/ui/Thumbnail/styles.d.ts.map +1 -1
  104. package/lib/typescript/src/components/ui/Thumbnail/types.d.ts +23 -5
  105. package/lib/typescript/src/components/ui/Thumbnail/types.d.ts.map +1 -1
  106. package/lib/typescript/src/components/ui/index.d.ts +1 -1
  107. package/lib/typescript/src/components/ui/index.d.ts.map +1 -1
  108. package/lib/typescript/src/components/ui/shared/pulse/constants.d.ts +2 -0
  109. package/lib/typescript/src/components/ui/shared/pulse/constants.d.ts.map +1 -0
  110. package/lib/typescript/src/components/ui/shared/pulse/normalizePulseOptions.d.ts +7 -0
  111. package/lib/typescript/src/components/ui/shared/pulse/normalizePulseOptions.d.ts.map +1 -0
  112. package/lib/typescript/src/components/ui/shared/pulse/types.d.ts +17 -0
  113. package/lib/typescript/src/components/ui/shared/pulse/types.d.ts.map +1 -0
  114. package/lib/typescript/src/components/ui/shared/pulse/usePulseAnimation.d.ts +8 -0
  115. package/lib/typescript/src/components/ui/shared/pulse/usePulseAnimation.d.ts.map +1 -0
  116. package/lib/typescript/src/components/ui/{Pulse → shared/pulse}/usePulseDriver.d.ts +1 -1
  117. package/lib/typescript/src/components/ui/shared/pulse/usePulseDriver.d.ts.map +1 -0
  118. package/lib/typescript/src/components/ui/shared/pulse/usePulseDriver.web.d.ts +7 -0
  119. package/lib/typescript/src/components/ui/shared/pulse/usePulseDriver.web.d.ts.map +1 -0
  120. package/package.json +2 -2
  121. package/src/components/ui/Avatar/Avatar.tsx +2 -6
  122. package/src/components/ui/Avatar/geometry.ts +30 -0
  123. package/src/components/ui/Avatar/styles.ts +2 -30
  124. package/src/components/ui/Avatar/types.ts +8 -2
  125. package/src/components/ui/AvatarGroup/AvatarGroup.tsx +1 -1
  126. package/src/components/ui/CircularProgress/CircularProgress.tsx +20 -26
  127. package/src/components/ui/CircularProgress/normalizeCircularProgress.ts +7 -15
  128. package/src/components/ui/CircularProgress/styles.ts +13 -0
  129. package/src/components/ui/CircularProgress/types.ts +19 -2
  130. package/src/components/ui/Pulse/PulseDot.tsx +6 -5
  131. package/src/components/ui/Pulse/constants.ts +14 -0
  132. package/src/components/ui/Pulse/types.ts +6 -6
  133. package/src/components/ui/Pulse/usePulse.ts +4 -81
  134. package/src/components/ui/Skeleton/Skeleton.tsx +6 -6
  135. package/src/components/ui/Skeleton/constants.ts +8 -0
  136. package/src/components/ui/Skeleton/types.ts +2 -2
  137. package/src/components/ui/TextField/TextFieldBase.tsx +28 -4
  138. package/src/components/ui/TextField/constants.ts +6 -0
  139. package/src/components/ui/TextField/styles.ts +8 -2
  140. package/src/components/ui/TextField/types.ts +41 -11
  141. package/src/components/ui/TextField/useMultilineLayout.ts +20 -0
  142. package/src/components/ui/TextField/useMultilineLayout.web.ts +59 -0
  143. package/src/components/ui/Textarea/types.ts +9 -6
  144. package/src/components/ui/Thumbnail/Thumbnail.tsx +19 -5
  145. package/src/components/ui/Thumbnail/constants.ts +24 -0
  146. package/src/components/ui/Thumbnail/index.ts +1 -0
  147. package/src/components/ui/Thumbnail/normalize.ts +38 -19
  148. package/src/components/ui/Thumbnail/styles.ts +6 -17
  149. package/src/components/ui/Thumbnail/types.ts +27 -5
  150. package/src/components/ui/index.ts +1 -0
  151. package/src/components/ui/shared/pulse/constants.ts +2 -0
  152. package/src/components/ui/shared/pulse/normalizePulseOptions.ts +48 -0
  153. package/src/components/ui/shared/pulse/types.ts +18 -0
  154. package/src/components/ui/shared/pulse/usePulseAnimation.ts +29 -0
  155. package/src/components/ui/{Pulse → shared/pulse}/usePulseDriver.ts +2 -2
  156. package/src/components/ui/{Pulse → shared/pulse}/usePulseDriver.web.ts +4 -19
  157. package/lib/module/components/ui/Pulse/normalizePulseOptions.js.map +0 -1
  158. package/lib/module/components/ui/Pulse/usePulseDriver.js.map +0 -1
  159. package/lib/module/components/ui/Pulse/usePulseDriver.web.js.map +0 -1
  160. package/lib/typescript/src/components/ui/Pulse/normalizePulseOptions.d.ts +0 -32
  161. package/lib/typescript/src/components/ui/Pulse/normalizePulseOptions.d.ts.map +0 -1
  162. package/lib/typescript/src/components/ui/Pulse/usePulseDriver.d.ts.map +0 -1
  163. package/lib/typescript/src/components/ui/Pulse/usePulseDriver.web.d.ts +0 -18
  164. package/lib/typescript/src/components/ui/Pulse/usePulseDriver.web.d.ts.map +0 -1
  165. package/src/components/ui/Pulse/normalizePulseOptions.ts +0 -76
@@ -0,0 +1,14 @@
1
+ import type { PulseDefaults } from '../shared/pulse/types';
2
+
3
+ export const PULSE_DEFAULTS: Readonly<PulseDefaults> = {
4
+ duration: 700,
5
+ delay: 0,
6
+ from: 0.6,
7
+ to: 1,
8
+ };
9
+ export const PULSE_DOT_DEFAULTS: Readonly<PulseDefaults> = {
10
+ duration: 700,
11
+ delay: 0,
12
+ from: 0.5,
13
+ to: 1,
14
+ };
@@ -1,6 +1,6 @@
1
1
  import type { StyleProp, ViewStyle } from 'react-native';
2
2
 
3
- export type PulseOptions = {
3
+ export interface PulseOptions {
4
4
  /** 透明度下界(默认 0.6) */
5
5
  from?: number;
6
6
  /** 透明度上界(默认 1) */
@@ -9,20 +9,20 @@ export type PulseOptions = {
9
9
  duration?: number;
10
10
  /** 首次周期之前的延迟(默认 0) */
11
11
  delay?: number;
12
- };
12
+ }
13
13
 
14
- export type PulseDotProps = PulseOptions & {
14
+ export interface PulseDotProps extends PulseOptions {
15
15
  size?: number;
16
16
  color?: string;
17
17
  /** 容器附加样式(margin / position 等布局微调)。 */
18
18
  style?: StyleProp<ViewStyle>;
19
19
  /** E2E / 测试定位 */
20
20
  testID?: string;
21
- };
21
+ }
22
22
 
23
- export type PulseProps = PulseOptions & {
23
+ export interface PulseProps extends PulseOptions {
24
24
  /** 容器附加样式(margin / position 等布局微调)。 */
25
25
  style?: StyleProp<ViewStyle>;
26
26
  /** E2E / 测试定位 */
27
27
  testID?: string;
28
- };
28
+ }
@@ -1,87 +1,10 @@
1
- import { useEffect, useMemo } from 'react';
2
1
  import { createLogger } from '../../../utils/logger';
3
- import { normalizePulseOptions } from './normalizePulseOptions';
4
- import type { PulseDefaults } from './normalizePulseOptions';
5
- import { usePulseDriver } from './usePulseDriver';
2
+ import { usePulseAnimation } from '../shared/pulse/usePulseAnimation';
3
+ import { PULSE_DEFAULTS } from './constants';
6
4
  import type { PulseOptions } from './types';
7
5
 
8
- /**
9
- * 在两个 opacity 值之间无限循环,返回可直接拼到 Animated.View 上的 style。
10
- *
11
- * 这是**唯一**的公共实现:参数校验、默认值与 dev 诊断全在这里完成,平台差异只下沉到
12
- * `usePulseDriver(.web).ts`。此前 native / web 各有一份 `usePulse`,两边各自读原始
13
- * props、各自解释默认值,web 侧还把未校验的 duration 直接喂给 `setInterval`。
14
- */
15
-
16
- const BASE_DEFAULTS: PulseDefaults = {
17
- duration: 700,
18
- delay: 0,
19
- from: 0.6,
20
- to: 1,
21
- };
22
-
23
- /** PulseDot / Skeleton 的默认值 —— 只有 from 更低,圆点 / 骨架淡到 0.5。 */
24
- export const DOT_DEFAULTS: PulseDefaults = {
25
- duration: 700,
26
- delay: 0,
27
- from: 0.5,
28
- to: 1,
29
- };
30
-
31
- // logger 在模块加载时一次性建好 —— render 期创建每次都会产生新对象。
32
- const pulseLog = createLogger('Pulse');
33
- const pulseDotLog = createLogger('PulseDot');
34
- const skeletonLog = createLogger('Skeleton');
35
-
36
- /** 诊断告警的固定 scope 集合。 */
37
- export type PulseScope = 'Pulse' | 'PulseDot' | 'Skeleton';
38
-
39
- function loggerForScope(scope: PulseScope) {
40
- switch (scope) {
41
- case 'Pulse':
42
- return pulseLog;
43
- case 'PulseDot':
44
- return pulseDotLog;
45
- case 'Skeleton':
46
- return skeletonLog;
47
- }
48
- }
49
-
50
- /** 内部:让 PulseDot / Skeleton 复用同一条归一化 + 诊断 + driver 链路。 */
51
- export function usePulseWithDefaults(
52
- options: PulseOptions | undefined,
53
- defaults: PulseDefaults,
54
- scope: PulseScope
55
- ) {
56
- // 依赖列表**故意**拆到 primitive:options / defaults 对象每次 render 都可能是新引用
57
- // (调用方常写内联字面量),直接依赖对象会让 memo 恒失效、并连带每帧重启 driver 的 effect。
58
- // 这八个 primitive 就是归一化的全部输入,拆开后覆盖完整、无遗漏。
59
- const normalized = useMemo(
60
- () => normalizePulseOptions(options, defaults),
61
- // eslint-disable-next-line react-hooks/exhaustive-deps
62
- [
63
- options?.duration,
64
- options?.delay,
65
- options?.from,
66
- options?.to,
67
- defaults.duration,
68
- defaults.delay,
69
- defaults.from,
70
- defaults.to,
71
- ]
72
- );
73
- useEffect(() => {
74
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
75
- for (const item of normalized.diagnostics) {
76
- loggerForScope(scope).warn(
77
- `${item.field}=${String(item.received)} 无效,已回退为 ${item.fallback}`
78
- );
79
- }
80
- }
81
- }, [normalized.diagnostics, scope]);
82
- return usePulseDriver(normalized);
83
- }
6
+ const log = createLogger('Pulse');
84
7
 
85
8
  export function usePulse(options: PulseOptions = {}) {
86
- return usePulseWithDefaults(options, BASE_DEFAULTS, 'Pulse');
9
+ return usePulseAnimation(options, PULSE_DEFAULTS, log);
87
10
  }
@@ -1,9 +1,13 @@
1
1
  import React from 'react';
2
2
  import Animated from 'react-native-reanimated';
3
- import { DOT_DEFAULTS, usePulseWithDefaults } from '../Pulse/usePulse';
3
+ import { createLogger } from '../../../utils/logger';
4
+ import { usePulseAnimation } from '../shared/pulse/usePulseAnimation';
5
+ import { SKELETON_DEFAULTS } from './constants';
4
6
  import { r, useColors } from '../../../theme';
5
7
  import type { SkeletonProps } from './types';
6
8
 
9
+ const log = createLogger('Skeleton');
10
+
7
11
  /**
8
12
  * 页面级加载占位骨架。3 个形状由 `shape` 切换。
9
13
  *
@@ -26,11 +30,7 @@ export function Skeleton({
26
30
  testID,
27
31
  }: SkeletonProps): React.JSX.Element {
28
32
  const c = useColors();
29
- const animatedStyle = usePulseWithDefaults(
30
- undefined,
31
- DOT_DEFAULTS,
32
- 'Skeleton'
33
- );
33
+ const animatedStyle = usePulseAnimation(undefined, SKELETON_DEFAULTS, log);
34
34
 
35
35
  // 按形状推默认尺寸 + 圆角;caller 显式传 width / height / radius 时 override
36
36
  const dim = (() => {
@@ -0,0 +1,8 @@
1
+ import type { PulseDefaults } from '../shared/pulse/types';
2
+
3
+ export const SKELETON_DEFAULTS: Readonly<PulseDefaults> = {
4
+ duration: 700,
5
+ delay: 0,
6
+ from: 0.5,
7
+ to: 1,
8
+ };
@@ -2,7 +2,7 @@ import type { DimensionValue, StyleProp, ViewStyle } from 'react-native';
2
2
 
3
3
  export type SkeletonShape = 'line' | 'rect' | 'circle';
4
4
 
5
- export type SkeletonProps = {
5
+ export interface SkeletonProps {
6
6
  /** 形状,默认 'rect'。
7
7
  * - 'line' → width 100% / height 11 / radius 3(文本占位)
8
8
  * - 'rect' → width 100% / height 80 / radius 8(图片或卡片占位)
@@ -19,4 +19,4 @@ export type SkeletonProps = {
19
19
  style?: StyleProp<ViewStyle>;
20
20
  /** E2E / 测试定位 */
21
21
  testID?: string;
22
- };
22
+ }
@@ -7,7 +7,7 @@ import React, {
7
7
  } from 'react';
8
8
  import { Platform, Text, TextInput, View } from 'react-native';
9
9
  import type { TextInputProps } from 'react-native';
10
- import { fixed, space, useColors, useThemedStyles } from '../../../theme';
10
+ import { fixed, useColors, useThemedStyles } from '../../../theme';
11
11
  import { childTestID } from '../../../utils/testID';
12
12
  import { createLogger } from '../../../utils/logger';
13
13
  import {
@@ -20,6 +20,7 @@ import { TextFieldSlot } from './TextFieldSlot';
20
20
  import type { TextFieldBaseProps, TextFieldHandle } from './types';
21
21
  import { useErrorAnnouncement } from './useErrorAnnouncement';
22
22
  import { useTextFieldValue } from './useTextFieldValue';
23
+ import { useMultilineLayout } from './useMultilineLayout';
23
24
 
24
25
  const log = createLogger('TextField');
25
26
 
@@ -49,6 +50,9 @@ export const TextFieldBase = forwardRef<TextFieldHandle, TextFieldBaseProps>(
49
50
  placeholderTextColor: callerPlaceholder,
50
51
  onFocus,
51
52
  onBlur,
53
+ onContentSizeChange,
54
+ onLayout,
55
+ scrollEnabled,
52
56
  testID,
53
57
  style: _style,
54
58
  numberOfLines: _numberOfLines,
@@ -94,6 +98,15 @@ export const TextFieldBase = forwardRef<TextFieldHandle, TextFieldBaseProps>(
94
98
  minHeight,
95
99
  maxHeight
96
100
  );
101
+ const multilineLayout = useMultilineLayout({
102
+ enabled: multiline,
103
+ value: controller.value,
104
+ minHeight: normalizedTextareaHeights.minHeight,
105
+ maxHeight: normalizedTextareaHeights.maxHeight,
106
+ fontSize: styles.input.fontSize,
107
+ placeholder: allowedNativeProps.placeholder as string | undefined,
108
+ inputRef: nativeRef,
109
+ });
97
110
  const sanitizedContainer = sanitizeTextFieldContainerStyle(containerStyle);
98
111
  const effectiveEditable = disabled !== true && editable !== false;
99
112
  const mergedAccessibilityState = {
@@ -193,6 +206,15 @@ export const TextFieldBase = forwardRef<TextFieldHandle, TextFieldBaseProps>(
193
206
  onChangeText={controller.onChangeText}
194
207
  editable={effectiveEditable}
195
208
  multiline={multiline}
209
+ onContentSizeChange={onContentSizeChange}
210
+ onLayout={(event) => {
211
+ if (multiline) multilineLayout.onLayout?.(event);
212
+ onLayout?.(event);
213
+ }}
214
+ scrollEnabled={
215
+ scrollEnabled ??
216
+ (multiline ? multilineLayout.scrollEnabled : undefined)
217
+ }
196
218
  onFocus={(event) => {
197
219
  setFocused(true);
198
220
  onFocus?.(event);
@@ -204,9 +226,11 @@ export const TextFieldBase = forwardRef<TextFieldHandle, TextFieldBaseProps>(
204
226
  style={[
205
227
  styles.input,
206
228
  multiline && styles.inputMultiline,
207
- multiline && {
208
- minHeight: Math.max(0, normalizedHeight - space[4] * 2),
209
- },
229
+ multiline &&
230
+ (multilineLayout.scrollEnabled
231
+ ? styles.inputScrolling
232
+ : styles.inputClipped),
233
+ multiline && multilineLayout.inputStyle,
210
234
  searchLayout !== undefined && {
211
235
  minHeight: searchLayout.interactiveHeight,
212
236
  },
@@ -0,0 +1,6 @@
1
+ import { space } from '../../../theme';
2
+
3
+ export const TEXT_FIELD_BORDER_WIDTH = 1;
4
+ export const TEXTAREA_VERTICAL_PADDING = space[4];
5
+ export const TEXTAREA_VERTICAL_INSET =
6
+ 2 * (TEXTAREA_VERTICAL_PADDING + TEXT_FIELD_BORDER_WIDTH);
@@ -1,6 +1,10 @@
1
1
  import { StyleSheet } from 'react-native';
2
2
  import { fw, radius, space, type as t } from '../../../theme';
3
3
  import type { ColorTokens } from '../../../theme';
4
+ import {
5
+ TEXTAREA_VERTICAL_PADDING,
6
+ TEXT_FIELD_BORDER_WIDTH,
7
+ } from './constants';
4
8
 
5
9
  /** TextField primitive styles —— Input + Textarea 共享。 */
6
10
  export const makeStyles = (c: ColorTokens) =>
@@ -11,10 +15,10 @@ export const makeStyles = (c: ColorTokens) =>
11
15
  gap: space[3],
12
16
  paddingHorizontal: space[5],
13
17
  borderRadius: radius.md,
14
- borderWidth: 1,
18
+ borderWidth: TEXT_FIELD_BORDER_WIDTH,
15
19
  },
16
20
  wrapMultiline: {
17
- paddingVertical: space[4],
21
+ paddingVertical: TEXTAREA_VERTICAL_PADDING,
18
22
  alignItems: 'flex-start',
19
23
  },
20
24
  /** Search 的可见底色/边框绝不承载命中区域。 */
@@ -59,6 +63,8 @@ export const makeStyles = (c: ColorTokens) =>
59
63
  inputMultiline: {
60
64
  textAlignVertical: 'top' as const,
61
65
  },
66
+ inputScrolling: { overflow: 'scroll' },
67
+ inputClipped: { overflow: 'hidden' },
62
68
  slotDisplay: {
63
69
  alignItems: 'center',
64
70
  justifyContent: 'center',
@@ -1,4 +1,11 @@
1
- import type { StyleProp, TextInputProps, ViewStyle } from 'react-native';
1
+ import type { ComponentRef, RefObject } from 'react';
2
+ import type { TextInput } from 'react-native';
3
+ import type {
4
+ StyleProp,
5
+ TextInputProps,
6
+ TextStyle,
7
+ ViewStyle,
8
+ } from 'react-native';
2
9
  import type { IconName } from '../Icon';
3
10
 
4
11
  /**
@@ -7,18 +14,18 @@ import type { IconName } from '../Icon';
7
14
  * `defaultValue?: never` 让「同时传 value 和 defaultValue」在类型层就报错 ——
8
15
  * 那是最常见的受控/非受控混用写法,运行时表现为「defaultValue 被静默忽略」。
9
16
  */
10
- export type ControlledTextValueProps = {
17
+ export interface ControlledTextValueProps {
11
18
  value: string;
12
19
  onChangeText: (value: string) => void;
13
20
  defaultValue?: never;
14
- };
21
+ }
15
22
 
16
23
  /** 非受控:内部持值,`defaultValue` 只在首次 render 读一次。 */
17
- export type UncontrolledTextValueProps = {
24
+ export interface UncontrolledTextValueProps {
18
25
  value?: never;
19
26
  defaultValue?: string;
20
27
  onChangeText?: (value: string) => void;
21
- };
28
+ }
22
29
 
23
30
  /** internal —— 由 InputProps / TextareaProps / SearchProps 组合进公共类型。 */
24
31
  export type TextFieldValueProps =
@@ -31,10 +38,10 @@ export type TextFieldValueProps =
31
38
  * 为什么不暴露原生 TextInput —— `clear()` 会绕过值状态机(受控下把 UI 清空但调用方
32
39
  * state 不变),`setNativeProps()` 能直接改任意原生属性,两者都会让公共契约失效。
33
40
  */
34
- export type TextFieldHandle = {
41
+ export interface TextFieldHandle {
35
42
  focus: () => void;
36
43
  blur: () => void;
37
- };
44
+ }
38
45
 
39
46
  /**
40
47
  * 左右 slot 的可验证配置。**不接受任意 ReactNode** ——
@@ -67,14 +74,14 @@ export type TextFieldContainerStyle = Omit<
67
74
  >;
68
75
 
69
76
  /** internal:Search 把真实交互行与仅作视觉的 surface 明确分层。 */
70
- export type SearchFieldLayout = {
77
+ export interface SearchFieldLayout {
71
78
  interactiveHeight: number;
72
79
  visibleHeight: number;
73
80
  verticalInset: number;
74
- };
81
+ }
75
82
 
76
83
  /** TextField 家族共享的非值 props(内部)。 */
77
- export type TextFieldCommonProps = {
84
+ export interface TextFieldCommonProps {
78
85
  /** 左侧 slot —— 图标 / 文本 / 带 handler 与名称的操作 */
79
86
  leading?: TextFieldSlot;
80
87
  /** 右侧 slot —— 图标 / 文本 / 带 handler 与名称的操作 */
@@ -85,7 +92,7 @@ export type TextFieldCommonProps = {
85
92
  disabled?: boolean;
86
93
  /** 外层容器样式;不接受会破坏最小 frame 的尺寸字段 */
87
94
  containerStyle?: StyleProp<TextFieldContainerStyle>;
88
- };
95
+ }
89
96
 
90
97
  /**
91
98
  * 这些原生 TextInput props 从公共类型删除:
@@ -127,3 +134,26 @@ export type TextFieldBaseProps = Omit<TextInputProps, RemovedTextInputProps> &
127
134
  /** internal:Search 的 44pt interactive row + 36pt decorative surface。 */
128
135
  searchLayout?: SearchFieldLayout;
129
136
  };
137
+
138
+ export interface MultilineLayoutInput {
139
+ enabled: boolean;
140
+ value: string;
141
+ minHeight: number;
142
+ maxHeight: number | undefined;
143
+ fontSize: number | undefined;
144
+ placeholder: string | undefined;
145
+ inputRef: RefObject<ComponentRef<typeof TextInput> | null>;
146
+ }
147
+
148
+ export interface MultilineLayout {
149
+ inputStyle: Pick<TextStyle, 'height' | 'minHeight' | 'maxHeight'>;
150
+ scrollEnabled: boolean;
151
+ onLayout?: TextInputProps['onLayout'];
152
+ }
153
+
154
+ /** RN Web 的公开 host ref 仅在平台实现内使用这两个 DOM 测量属性。 */
155
+ export interface WebTextInputMeasurement {
156
+ scrollHeight: number;
157
+ scrollTop: number;
158
+ style: { height: string; minHeight: string };
159
+ }
@@ -0,0 +1,20 @@
1
+ import { TEXTAREA_VERTICAL_INSET } from './constants';
2
+ import type { MultilineLayout, MultilineLayoutInput } from './types';
3
+
4
+ /** 原生 Yoga 直接测量文本;固定 height 会阻断 iOS Fabric 的后续内容尺寸通知。 */
5
+ export function useMultilineLayout({
6
+ minHeight,
7
+ maxHeight,
8
+ }: MultilineLayoutInput): MultilineLayout {
9
+ return {
10
+ inputStyle: {
11
+ minHeight: Math.max(0, minHeight - TEXTAREA_VERTICAL_INSET),
12
+ maxHeight:
13
+ maxHeight === undefined
14
+ ? undefined
15
+ : Math.max(0, maxHeight - TEXTAREA_VERTICAL_INSET),
16
+ },
17
+ // 原生仅在内容溢出时滚动;min=max 时也不等待不会再变化的框高事件。
18
+ scrollEnabled: true,
19
+ };
20
+ }
@@ -0,0 +1,59 @@
1
+ import { useCallback, useLayoutEffect, useState } from 'react';
2
+ import { TEXTAREA_VERTICAL_INSET } from './constants';
3
+ import type {
4
+ MultilineLayout,
5
+ MultilineLayoutInput,
6
+ WebTextInputMeasurement,
7
+ } from './types';
8
+
9
+ /** RNW 的 contentSize 使用 scrollHeight,固定高度会阻止缩短,须临时解除高度后测量。 */
10
+ export function useMultilineLayout({
11
+ enabled,
12
+ value,
13
+ minHeight,
14
+ maxHeight,
15
+ fontSize,
16
+ placeholder,
17
+ inputRef,
18
+ }: MultilineLayoutInput): MultilineLayout {
19
+ const [contentHeight, setContentHeight] = useState(0);
20
+ const [width, setWidth] = useState(0);
21
+ const minimum = Math.max(0, minHeight - TEXTAREA_VERTICAL_INSET);
22
+ const maximum =
23
+ maxHeight === undefined
24
+ ? Infinity
25
+ : Math.max(0, maxHeight - TEXTAREA_VERTICAL_INSET);
26
+ useLayoutEffect(() => {
27
+ if (!enabled || inputRef.current === null) return;
28
+ const node = inputRef.current as unknown as WebTextInputMeasurement;
29
+ const { height, minHeight: previousMinimum } = node.style;
30
+ const scrollTop = node.scrollTop;
31
+ node.style.height = '0px';
32
+ node.style.minHeight = '0px';
33
+ const measured = node.scrollHeight;
34
+ node.style.height = height;
35
+ node.style.minHeight = previousMinimum;
36
+ node.scrollTop = scrollTop;
37
+ if (Number.isFinite(measured)) setContentHeight(measured);
38
+ }, [
39
+ enabled,
40
+ value,
41
+ width,
42
+ fontSize,
43
+ placeholder,
44
+ minHeight,
45
+ maxHeight,
46
+ inputRef,
47
+ ]);
48
+ const onLayout = useCallback<NonNullable<MultilineLayout['onLayout']>>(
49
+ (event) => setWidth(event.nativeEvent.layout.width),
50
+ []
51
+ );
52
+ return {
53
+ inputStyle: {
54
+ height: Math.min(maximum, Math.max(minimum, contentHeight)),
55
+ },
56
+ scrollEnabled: contentHeight > maximum,
57
+ onLayout,
58
+ };
59
+ }
@@ -13,9 +13,12 @@ import type {
13
13
  */
14
14
  export type TextareaProps = Omit<TextInputProps, RemovedTextInputProps> &
15
15
  TextFieldCommonProps &
16
- TextFieldValueProps & {
17
- /** 最小高度,默认 96(约 5 行) */
18
- minHeight?: number;
19
- /** 最大高度(超过后内部滚动),默认不限制 */
20
- maxHeight?: number;
21
- };
16
+ TextFieldValueProps &
17
+ TextareaHeightProps;
18
+
19
+ interface TextareaHeightProps {
20
+ /** 最小高度,默认 96(约 5 行) */
21
+ minHeight?: number;
22
+ /** 最大高度(超过后内部滚动),默认不限制 */
23
+ maxHeight?: number;
24
+ }
@@ -5,8 +5,11 @@ import { resolveImageSource } from '../../../utils/imageSource';
5
5
  import { createLogger } from '../../../utils/logger';
6
6
  import { ImageAttempt } from '../shared/ImageAttempt';
7
7
  import { A11Y_HIDDEN_PROPS } from '../shared/a11y';
8
- import { sanitizeThumbnailImageStyle } from './normalize';
9
- import { makeStyles, sizingFor } from './styles';
8
+ import {
9
+ normalizeThumbnailDimensions,
10
+ sanitizeThumbnailImageStyle,
11
+ } from './normalize';
12
+ import { makeStyles } from './styles';
10
13
  import type { ThumbnailProps } from './types';
11
14
 
12
15
  const log = createLogger('Thumbnail');
@@ -22,6 +25,7 @@ export function Thumbnail(props: ThumbnailProps): React.JSX.Element {
22
25
  uri,
23
26
  source,
24
27
  size = 'md',
28
+ fallback,
25
29
  selected,
26
30
  containerStyle,
27
31
  imageStyle,
@@ -31,7 +35,8 @@ export function Thumbnail(props: ThumbnailProps): React.JSX.Element {
31
35
  } = props;
32
36
  const colors = useColors();
33
37
  const styles = useThemedStyles(makeStyles);
34
- const dim = sizingFor(size);
38
+ const normalizedDimensions = normalizeThumbnailDimensions(size);
39
+ const dim = normalizedDimensions.dimensions;
35
40
  const hasUri = hasOwn(props, 'uri');
36
41
  const hasSource = hasOwn(props, 'source');
37
42
  const sourceCandidate =
@@ -43,6 +48,7 @@ export function Thumbnail(props: ThumbnailProps): React.JSX.Element {
43
48
  const resolvedSource = resolveImageSource(sourceCandidate);
44
49
  const sanitizedImageStyle = sanitizeThumbnailImageStyle(imageStyle);
45
50
  const diagnostics = [
51
+ ...normalizedDimensions.diagnostics,
46
52
  ...(resolvedSource === undefined ? ['source'] : []),
47
53
  ...sanitizedImageStyle.diagnostics.map((field) =>
48
54
  field === 'style' ? 'imageStyle' : `imageStyle.${field}`
@@ -72,16 +78,24 @@ export function Thumbnail(props: ThumbnailProps): React.JSX.Element {
72
78
  borderRadius: dim.borderRadius,
73
79
  borderColor: selected ? colors.primary : 'transparent',
74
80
  };
81
+ const fallbackContent =
82
+ fallback === undefined ? null : (
83
+ <View {...imageAccessibilityProps} style={styles.fallback}>
84
+ {fallback}
85
+ </View>
86
+ );
75
87
 
76
88
  return (
77
89
  <View style={containerStyle} testID={testID}>
78
90
  <View style={[styles.visualFrame, dim]}>
79
- {resolvedSource === undefined ? null : (
91
+ {resolvedSource === undefined ? (
92
+ fallbackContent
93
+ ) : (
80
94
  <ImageAttempt
81
95
  {...imageAccessibilityProps}
82
96
  key={resolvedSource.key}
83
97
  source={resolvedSource.source}
84
- fallback={null}
98
+ fallback={fallbackContent}
85
99
  style={[sanitizedImageStyle.style, StyleSheet.absoluteFill]}
86
100
  resizeMode={resizeMode}
87
101
  />
@@ -0,0 +1,24 @@
1
+ import { r } from '../../../theme';
2
+ import type { ThumbnailDimensions, ThumbnailSize } from './types';
3
+
4
+ export const THUMBNAIL_DIMENSIONS: Readonly<
5
+ Record<ThumbnailSize, Required<ThumbnailDimensions>>
6
+ > = {
7
+ sm: { width: r(64), height: r(40), borderRadius: r(6) },
8
+ md: { width: r(113), height: r(67), borderRadius: r(8) },
9
+ lg: { width: r(160), height: r(96), borderRadius: r(10) },
10
+ };
11
+
12
+ export const RESERVED_IMAGE_STYLE_KEYS = [
13
+ 'position',
14
+ 'top',
15
+ 'right',
16
+ 'bottom',
17
+ 'left',
18
+ 'width',
19
+ 'height',
20
+ 'minWidth',
21
+ 'minHeight',
22
+ 'maxWidth',
23
+ 'maxHeight',
24
+ ] as const;
@@ -1,5 +1,6 @@
1
1
  export { Thumbnail } from './Thumbnail';
2
2
  export type {
3
+ ThumbnailDimensions,
3
4
  ThumbnailImageStyle,
4
5
  ThumbnailProps,
5
6
  ThumbnailSize,
@@ -1,25 +1,44 @@
1
1
  import { StyleSheet } from 'react-native';
2
2
  import type { StyleProp } from 'react-native';
3
- import type { ThumbnailImageStyle } from './types';
3
+ import type {
4
+ NormalizedThumbnailDimensions,
5
+ SanitizedThumbnailImageStyle,
6
+ ThumbnailImageStyle,
7
+ } from './types';
8
+ import { RESERVED_IMAGE_STYLE_KEYS, THUMBNAIL_DIMENSIONS } from './constants';
4
9
 
5
- const RESERVED_IMAGE_STYLE_KEYS = [
6
- 'position',
7
- 'top',
8
- 'right',
9
- 'bottom',
10
- 'left',
11
- 'width',
12
- 'height',
13
- 'minWidth',
14
- 'minHeight',
15
- 'maxWidth',
16
- 'maxHeight',
17
- ] as const;
18
-
19
- type SanitizedThumbnailImageStyle = {
20
- style: ThumbnailImageStyle;
21
- diagnostics: readonly string[];
22
- };
10
+ export function normalizeThumbnailDimensions(
11
+ size: unknown
12
+ ): NormalizedThumbnailDimensions {
13
+ if (size === undefined || size === 'sm' || size === 'md' || size === 'lg') {
14
+ return { dimensions: THUMBNAIL_DIMENSIONS[size ?? 'md'], diagnostics: [] };
15
+ }
16
+ if (typeof size === 'object' && size !== null && !Array.isArray(size)) {
17
+ const { width, height, borderRadius } = size as Record<string, unknown>;
18
+ if (
19
+ typeof width === 'number' &&
20
+ Number.isFinite(width) &&
21
+ width > 0 &&
22
+ typeof height === 'number' &&
23
+ Number.isFinite(height) &&
24
+ height > 0 &&
25
+ (borderRadius === undefined ||
26
+ (typeof borderRadius === 'number' &&
27
+ Number.isFinite(borderRadius) &&
28
+ borderRadius >= 0))
29
+ ) {
30
+ return {
31
+ dimensions: {
32
+ width,
33
+ height,
34
+ borderRadius: borderRadius ?? THUMBNAIL_DIMENSIONS.md.borderRadius,
35
+ },
36
+ diagnostics: [],
37
+ };
38
+ }
39
+ }
40
+ return { dimensions: THUMBNAIL_DIMENSIONS.md, diagnostics: ['size'] };
41
+ }
23
42
 
24
43
  function invalidStyle(): SanitizedThumbnailImageStyle {
25
44
  return { style: {}, diagnostics: ['style'] };