@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.
- package/README.md +23 -171
- package/lib/module/components/ui/Avatar/Avatar.js +2 -1
- package/lib/module/components/ui/Avatar/Avatar.js.map +1 -1
- package/lib/module/components/ui/Avatar/geometry.js +43 -0
- package/lib/module/components/ui/Avatar/geometry.js.map +1 -0
- package/lib/module/components/ui/Avatar/styles.js +1 -41
- package/lib/module/components/ui/Avatar/styles.js.map +1 -1
- package/lib/module/components/ui/AvatarGroup/AvatarGroup.js +1 -1
- package/lib/module/components/ui/AvatarGroup/AvatarGroup.js.map +1 -1
- package/lib/module/components/ui/CircularProgress/CircularProgress.js +14 -25
- package/lib/module/components/ui/CircularProgress/CircularProgress.js.map +1 -1
- package/lib/module/components/ui/CircularProgress/normalizeCircularProgress.js.map +1 -1
- package/lib/module/components/ui/CircularProgress/styles.js +24 -0
- package/lib/module/components/ui/CircularProgress/styles.js.map +1 -0
- package/lib/module/components/ui/Pulse/PulseDot.js +5 -5
- package/lib/module/components/ui/Pulse/PulseDot.js.map +1 -1
- package/lib/module/components/ui/Pulse/constants.js +15 -0
- package/lib/module/components/ui/Pulse/constants.js.map +1 -0
- package/lib/module/components/ui/Pulse/usePulse.js +4 -62
- package/lib/module/components/ui/Pulse/usePulse.js.map +1 -1
- package/lib/module/components/ui/Skeleton/Skeleton.js +6 -2
- package/lib/module/components/ui/Skeleton/Skeleton.js.map +1 -1
- package/lib/module/components/ui/Skeleton/constants.js +9 -0
- package/lib/module/components/ui/Skeleton/constants.js.map +1 -0
- package/lib/module/components/ui/TextField/TextFieldBase.js +21 -4
- package/lib/module/components/ui/TextField/TextFieldBase.js.map +1 -1
- package/lib/module/components/ui/TextField/constants.js +7 -0
- package/lib/module/components/ui/TextField/constants.js.map +1 -0
- package/lib/module/components/ui/TextField/styles.js +10 -2
- package/lib/module/components/ui/TextField/styles.js.map +1 -1
- package/lib/module/components/ui/TextField/useMultilineLayout.js +18 -0
- package/lib/module/components/ui/TextField/useMultilineLayout.js.map +1 -0
- package/lib/module/components/ui/TextField/useMultilineLayout.web.js +44 -0
- package/lib/module/components/ui/TextField/useMultilineLayout.web.js.map +1 -0
- package/lib/module/components/ui/Thumbnail/Thumbnail.js +13 -6
- package/lib/module/components/ui/Thumbnail/Thumbnail.js.map +1 -1
- package/lib/module/components/ui/Thumbnail/constants.js +22 -0
- package/lib/module/components/ui/Thumbnail/constants.js.map +1 -0
- package/lib/module/components/ui/Thumbnail/normalize.js +30 -1
- package/lib/module/components/ui/Thumbnail/normalize.js.map +1 -1
- package/lib/module/components/ui/Thumbnail/styles.js +5 -28
- package/lib/module/components/ui/Thumbnail/styles.js.map +1 -1
- package/lib/module/components/ui/index.js.map +1 -1
- package/lib/module/components/ui/shared/pulse/constants.js +5 -0
- package/lib/module/components/ui/shared/pulse/constants.js.map +1 -0
- package/lib/module/components/ui/{Pulse → shared/pulse}/normalizePulseOptions.js +2 -12
- package/lib/module/components/ui/shared/pulse/normalizePulseOptions.js.map +1 -0
- package/lib/module/components/ui/shared/pulse/types.js +2 -0
- package/lib/module/components/ui/shared/pulse/types.js.map +1 -0
- package/lib/module/components/ui/shared/pulse/usePulseAnimation.js +30 -0
- package/lib/module/components/ui/shared/pulse/usePulseAnimation.js.map +1 -0
- package/lib/module/components/ui/{Pulse → shared/pulse}/usePulseDriver.js +1 -1
- package/lib/module/components/ui/shared/pulse/usePulseDriver.js.map +1 -0
- package/lib/module/components/ui/{Pulse → shared/pulse}/usePulseDriver.web.js +2 -17
- package/lib/module/components/ui/shared/pulse/usePulseDriver.web.js.map +1 -0
- package/lib/typescript/src/components/ui/Avatar/Avatar.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Avatar/geometry.d.ts +7 -0
- package/lib/typescript/src/components/ui/Avatar/geometry.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Avatar/styles.d.ts +1 -9
- package/lib/typescript/src/components/ui/Avatar/styles.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Avatar/types.d.ts +7 -2
- package/lib/typescript/src/components/ui/Avatar/types.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/CircularProgress/CircularProgress.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/CircularProgress/normalizeCircularProgress.d.ts +2 -14
- package/lib/typescript/src/components/ui/CircularProgress/normalizeCircularProgress.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/CircularProgress/styles.d.ts +19 -0
- package/lib/typescript/src/components/ui/CircularProgress/styles.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/CircularProgress/types.d.ts +17 -2
- package/lib/typescript/src/components/ui/CircularProgress/types.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Pulse/PulseDot.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Pulse/constants.d.ts +4 -0
- package/lib/typescript/src/components/ui/Pulse/constants.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Pulse/types.d.ts +6 -6
- package/lib/typescript/src/components/ui/Pulse/types.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Pulse/usePulse.d.ts +0 -9
- package/lib/typescript/src/components/ui/Pulse/usePulse.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Skeleton/Skeleton.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Skeleton/constants.d.ts +3 -0
- package/lib/typescript/src/components/ui/Skeleton/constants.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Skeleton/types.d.ts +2 -2
- package/lib/typescript/src/components/ui/Skeleton/types.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/TextField/TextFieldBase.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/TextField/constants.d.ts +4 -0
- package/lib/typescript/src/components/ui/TextField/constants.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/TextField/styles.d.ts +6 -0
- package/lib/typescript/src/components/ui/TextField/styles.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/TextField/types.d.ts +36 -11
- package/lib/typescript/src/components/ui/TextField/types.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/TextField/useMultilineLayout.d.ts +4 -0
- package/lib/typescript/src/components/ui/TextField/useMultilineLayout.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/TextField/useMultilineLayout.web.d.ts +4 -0
- package/lib/typescript/src/components/ui/TextField/useMultilineLayout.web.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Textarea/types.d.ts +4 -2
- package/lib/typescript/src/components/ui/Textarea/types.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Thumbnail/constants.d.ts +4 -0
- package/lib/typescript/src/components/ui/Thumbnail/constants.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Thumbnail/index.d.ts +1 -1
- package/lib/typescript/src/components/ui/Thumbnail/index.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Thumbnail/normalize.d.ts +2 -6
- package/lib/typescript/src/components/ui/Thumbnail/normalize.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Thumbnail/styles.d.ts +9 -8
- package/lib/typescript/src/components/ui/Thumbnail/styles.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Thumbnail/types.d.ts +23 -5
- package/lib/typescript/src/components/ui/Thumbnail/types.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/index.d.ts +1 -1
- package/lib/typescript/src/components/ui/index.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/shared/pulse/constants.d.ts +2 -0
- package/lib/typescript/src/components/ui/shared/pulse/constants.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/shared/pulse/normalizePulseOptions.d.ts +7 -0
- package/lib/typescript/src/components/ui/shared/pulse/normalizePulseOptions.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/shared/pulse/types.d.ts +17 -0
- package/lib/typescript/src/components/ui/shared/pulse/types.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/shared/pulse/usePulseAnimation.d.ts +8 -0
- package/lib/typescript/src/components/ui/shared/pulse/usePulseAnimation.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/{Pulse → shared/pulse}/usePulseDriver.d.ts +1 -1
- package/lib/typescript/src/components/ui/shared/pulse/usePulseDriver.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/shared/pulse/usePulseDriver.web.d.ts +7 -0
- package/lib/typescript/src/components/ui/shared/pulse/usePulseDriver.web.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/components/ui/Avatar/Avatar.tsx +2 -6
- package/src/components/ui/Avatar/geometry.ts +30 -0
- package/src/components/ui/Avatar/styles.ts +2 -30
- package/src/components/ui/Avatar/types.ts +8 -2
- package/src/components/ui/AvatarGroup/AvatarGroup.tsx +1 -1
- package/src/components/ui/CircularProgress/CircularProgress.tsx +20 -26
- package/src/components/ui/CircularProgress/normalizeCircularProgress.ts +7 -15
- package/src/components/ui/CircularProgress/styles.ts +13 -0
- package/src/components/ui/CircularProgress/types.ts +19 -2
- package/src/components/ui/Pulse/PulseDot.tsx +6 -5
- package/src/components/ui/Pulse/constants.ts +14 -0
- package/src/components/ui/Pulse/types.ts +6 -6
- package/src/components/ui/Pulse/usePulse.ts +4 -81
- package/src/components/ui/Skeleton/Skeleton.tsx +6 -6
- package/src/components/ui/Skeleton/constants.ts +8 -0
- package/src/components/ui/Skeleton/types.ts +2 -2
- package/src/components/ui/TextField/TextFieldBase.tsx +28 -4
- package/src/components/ui/TextField/constants.ts +6 -0
- package/src/components/ui/TextField/styles.ts +8 -2
- package/src/components/ui/TextField/types.ts +41 -11
- package/src/components/ui/TextField/useMultilineLayout.ts +20 -0
- package/src/components/ui/TextField/useMultilineLayout.web.ts +59 -0
- package/src/components/ui/Textarea/types.ts +9 -6
- package/src/components/ui/Thumbnail/Thumbnail.tsx +19 -5
- package/src/components/ui/Thumbnail/constants.ts +24 -0
- package/src/components/ui/Thumbnail/index.ts +1 -0
- package/src/components/ui/Thumbnail/normalize.ts +38 -19
- package/src/components/ui/Thumbnail/styles.ts +6 -17
- package/src/components/ui/Thumbnail/types.ts +27 -5
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/shared/pulse/constants.ts +2 -0
- package/src/components/ui/shared/pulse/normalizePulseOptions.ts +48 -0
- package/src/components/ui/shared/pulse/types.ts +18 -0
- package/src/components/ui/shared/pulse/usePulseAnimation.ts +29 -0
- package/src/components/ui/{Pulse → shared/pulse}/usePulseDriver.ts +2 -2
- package/src/components/ui/{Pulse → shared/pulse}/usePulseDriver.web.ts +4 -19
- package/lib/module/components/ui/Pulse/normalizePulseOptions.js.map +0 -1
- package/lib/module/components/ui/Pulse/usePulseDriver.js.map +0 -1
- package/lib/module/components/ui/Pulse/usePulseDriver.web.js.map +0 -1
- package/lib/typescript/src/components/ui/Pulse/normalizePulseOptions.d.ts +0 -32
- package/lib/typescript/src/components/ui/Pulse/normalizePulseOptions.d.ts.map +0 -1
- package/lib/typescript/src/components/ui/Pulse/usePulseDriver.d.ts.map +0 -1
- package/lib/typescript/src/components/ui/Pulse/usePulseDriver.web.d.ts +0 -18
- package/lib/typescript/src/components/ui/Pulse/usePulseDriver.web.d.ts.map +0 -1
- 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
|
|
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
|
|
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
|
|
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 {
|
|
4
|
-
import
|
|
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
|
|
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 {
|
|
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 =
|
|
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 = (() => {
|
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
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
|
},
|
|
@@ -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:
|
|
18
|
+
borderWidth: TEXT_FIELD_BORDER_WIDTH,
|
|
15
19
|
},
|
|
16
20
|
wrapMultiline: {
|
|
17
|
-
paddingVertical:
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 {
|
|
9
|
-
|
|
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
|
|
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 ?
|
|
91
|
+
{resolvedSource === undefined ? (
|
|
92
|
+
fallbackContent
|
|
93
|
+
) : (
|
|
80
94
|
<ImageAttempt
|
|
81
95
|
{...imageAccessibilityProps}
|
|
82
96
|
key={resolvedSource.key}
|
|
83
97
|
source={resolvedSource.source}
|
|
84
|
-
fallback={
|
|
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,25 +1,44 @@
|
|
|
1
1
|
import { StyleSheet } from 'react-native';
|
|
2
2
|
import type { StyleProp } from 'react-native';
|
|
3
|
-
import type {
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
'
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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'] };
|