@oxyhq/bloom 0.27.0 → 0.28.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.
- package/lib/commonjs/animated-check/AnimatedCheck.js +162 -0
- package/lib/commonjs/animated-check/AnimatedCheck.js.map +1 -0
- package/lib/commonjs/animated-check/index.js +13 -0
- package/lib/commonjs/animated-check/index.js.map +1 -0
- package/lib/commonjs/dialog/placement.js +9 -7
- package/lib/commonjs/dialog/placement.js.map +1 -1
- package/lib/commonjs/fill/index.js.map +1 -1
- package/lib/commonjs/hooks/index.js +20 -0
- package/lib/commonjs/hooks/index.js.map +1 -1
- package/lib/commonjs/hooks/useGutters.js +106 -0
- package/lib/commonjs/hooks/useGutters.js.map +1 -0
- package/lib/commonjs/hooks/useHaptics.js +69 -0
- package/lib/commonjs/hooks/useHaptics.js.map +1 -0
- package/lib/commonjs/hooks/useInteractionState.js +8 -2
- package/lib/commonjs/hooks/useInteractionState.js.map +1 -1
- package/lib/commonjs/index.js +155 -63
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +155 -63
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/media-inset-border/MediaInsetBorder.js +50 -0
- package/lib/commonjs/media-inset-border/MediaInsetBorder.js.map +1 -0
- package/lib/commonjs/media-inset-border/index.js +13 -0
- package/lib/commonjs/media-inset-border/index.js.map +1 -0
- package/lib/commonjs/motion/ScreenTransition.js +46 -0
- package/lib/commonjs/motion/ScreenTransition.js.map +1 -0
- package/lib/commonjs/motion/index.js +32 -0
- package/lib/commonjs/motion/index.js.map +1 -0
- package/lib/commonjs/motion/motion.js +93 -0
- package/lib/commonjs/motion/motion.js.map +1 -0
- package/lib/commonjs/pressable-scale/PressableScale.js +75 -0
- package/lib/commonjs/pressable-scale/PressableScale.js.map +1 -0
- package/lib/commonjs/pressable-scale/index.js +13 -0
- package/lib/commonjs/pressable-scale/index.js.map +1 -0
- package/lib/commonjs/pressable-with-hover/PressableWithHover.js +36 -0
- package/lib/commonjs/pressable-with-hover/PressableWithHover.js.map +1 -0
- package/lib/commonjs/pressable-with-hover/index.js +13 -0
- package/lib/commonjs/pressable-with-hover/index.js.map +1 -0
- package/lib/commonjs/styles/breakpoints.js +24 -0
- package/lib/commonjs/styles/breakpoints.js.map +1 -0
- package/lib/commonjs/styles/index.js +7 -0
- package/lib/commonjs/styles/index.js.map +1 -1
- package/lib/commonjs/subtle-hover/SubtleHover.js +69 -0
- package/lib/commonjs/subtle-hover/SubtleHover.js.map +1 -0
- package/lib/commonjs/subtle-hover/index.js +13 -0
- package/lib/commonjs/subtle-hover/index.js.map +1 -0
- package/lib/commonjs/theme/build-theme.js +2 -0
- package/lib/commonjs/theme/build-theme.js.map +1 -1
- package/lib/commonjs/theme/gradients.js +39 -0
- package/lib/commonjs/theme/gradients.js.map +1 -0
- package/lib/commonjs/theme/index.js +7 -0
- package/lib/commonjs/theme/index.js.map +1 -1
- package/lib/module/animated-check/AnimatedCheck.js +157 -0
- package/lib/module/animated-check/AnimatedCheck.js.map +1 -0
- package/lib/module/animated-check/index.js +4 -0
- package/lib/module/animated-check/index.js.map +1 -0
- package/lib/module/dialog/placement.js +9 -7
- package/lib/module/dialog/placement.js.map +1 -1
- package/lib/module/fill/index.js.map +1 -1
- package/lib/module/hooks/index.js +2 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/useGutters.js +103 -0
- package/lib/module/hooks/useGutters.js.map +1 -0
- package/lib/module/hooks/useHaptics.js +65 -0
- package/lib/module/hooks/useHaptics.js.map +1 -0
- package/lib/module/hooks/useInteractionState.js +10 -3
- package/lib/module/hooks/useInteractionState.js.map +1 -1
- package/lib/module/index.js +10 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +10 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/media-inset-border/MediaInsetBorder.js +47 -0
- package/lib/module/media-inset-border/MediaInsetBorder.js.map +1 -0
- package/lib/module/media-inset-border/index.js +4 -0
- package/lib/module/media-inset-border/index.js.map +1 -0
- package/lib/module/motion/ScreenTransition.js +40 -0
- package/lib/module/motion/ScreenTransition.js.map +1 -0
- package/lib/module/motion/index.js +5 -0
- package/lib/module/motion/index.js.map +1 -0
- package/lib/module/motion/motion.js +87 -0
- package/lib/module/motion/motion.js.map +1 -0
- package/lib/module/pressable-scale/PressableScale.js +72 -0
- package/lib/module/pressable-scale/PressableScale.js.map +1 -0
- package/lib/module/pressable-scale/index.js +4 -0
- package/lib/module/pressable-scale/index.js.map +1 -0
- package/lib/module/pressable-with-hover/PressableWithHover.js +31 -0
- package/lib/module/pressable-with-hover/PressableWithHover.js.map +1 -0
- package/lib/module/pressable-with-hover/index.js +4 -0
- package/lib/module/pressable-with-hover/index.js.map +1 -0
- package/lib/module/styles/breakpoints.js +20 -0
- package/lib/module/styles/breakpoints.js.map +1 -0
- package/lib/module/styles/index.js +1 -0
- package/lib/module/styles/index.js.map +1 -1
- package/lib/module/subtle-hover/SubtleHover.js +66 -0
- package/lib/module/subtle-hover/SubtleHover.js.map +1 -0
- package/lib/module/subtle-hover/index.js +4 -0
- package/lib/module/subtle-hover/index.js.map +1 -0
- package/lib/module/theme/build-theme.js +2 -0
- package/lib/module/theme/build-theme.js.map +1 -1
- package/lib/module/theme/gradients.js +35 -0
- package/lib/module/theme/gradients.js.map +1 -0
- package/lib/module/theme/index.js +1 -0
- package/lib/module/theme/index.js.map +1 -1
- package/lib/typescript/commonjs/animated-check/AnimatedCheck.d.ts +18 -0
- package/lib/typescript/commonjs/animated-check/AnimatedCheck.d.ts.map +1 -0
- package/lib/typescript/commonjs/animated-check/index.d.ts +3 -0
- package/lib/typescript/commonjs/animated-check/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/dialog/placement.d.ts +8 -7
- package/lib/typescript/commonjs/dialog/placement.d.ts.map +1 -1
- package/lib/typescript/commonjs/fill/index.d.ts +2 -2
- package/lib/typescript/commonjs/fill/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/hooks/index.d.ts +4 -0
- package/lib/typescript/commonjs/hooks/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/hooks/useGutters.d.ts +32 -0
- package/lib/typescript/commonjs/hooks/useGutters.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/useHaptics.d.ts +28 -0
- package/lib/typescript/commonjs/hooks/useHaptics.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/useInteractionState.d.ts +6 -0
- package/lib/typescript/commonjs/hooks/useInteractionState.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +16 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +16 -0
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/media-inset-border/MediaInsetBorder.d.ts +26 -0
- package/lib/typescript/commonjs/media-inset-border/MediaInsetBorder.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-inset-border/index.d.ts +3 -0
- package/lib/typescript/commonjs/media-inset-border/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/motion/ScreenTransition.d.ts +33 -0
- package/lib/typescript/commonjs/motion/ScreenTransition.d.ts.map +1 -0
- package/lib/typescript/commonjs/motion/index.d.ts +4 -0
- package/lib/typescript/commonjs/motion/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/motion/motion.d.ts +24 -0
- package/lib/typescript/commonjs/motion/motion.d.ts.map +1 -0
- package/lib/typescript/commonjs/pressable-scale/PressableScale.d.ts +21 -0
- package/lib/typescript/commonjs/pressable-scale/PressableScale.d.ts.map +1 -0
- package/lib/typescript/commonjs/pressable-scale/index.d.ts +3 -0
- package/lib/typescript/commonjs/pressable-scale/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/pressable-with-hover/PressableWithHover.d.ts +18 -0
- package/lib/typescript/commonjs/pressable-with-hover/PressableWithHover.d.ts.map +1 -0
- package/lib/typescript/commonjs/pressable-with-hover/index.d.ts +3 -0
- package/lib/typescript/commonjs/pressable-with-hover/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/styles/breakpoints.d.ts +18 -0
- package/lib/typescript/commonjs/styles/breakpoints.d.ts.map +1 -0
- package/lib/typescript/commonjs/styles/index.d.ts +2 -0
- package/lib/typescript/commonjs/styles/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/subtle-hover/SubtleHover.d.ts +32 -0
- package/lib/typescript/commonjs/subtle-hover/SubtleHover.d.ts.map +1 -0
- package/lib/typescript/commonjs/subtle-hover/index.d.ts +3 -0
- package/lib/typescript/commonjs/subtle-hover/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/build-theme.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/gradients.d.ts +12 -0
- package/lib/typescript/commonjs/theme/gradients.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/index.d.ts +2 -1
- package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/types.d.ts +11 -0
- package/lib/typescript/commonjs/theme/types.d.ts.map +1 -1
- package/lib/typescript/module/animated-check/AnimatedCheck.d.ts +18 -0
- package/lib/typescript/module/animated-check/AnimatedCheck.d.ts.map +1 -0
- package/lib/typescript/module/animated-check/index.d.ts +3 -0
- package/lib/typescript/module/animated-check/index.d.ts.map +1 -0
- package/lib/typescript/module/dialog/placement.d.ts +8 -7
- package/lib/typescript/module/dialog/placement.d.ts.map +1 -1
- package/lib/typescript/module/fill/index.d.ts +2 -2
- package/lib/typescript/module/fill/index.d.ts.map +1 -1
- package/lib/typescript/module/hooks/index.d.ts +4 -0
- package/lib/typescript/module/hooks/index.d.ts.map +1 -1
- package/lib/typescript/module/hooks/useGutters.d.ts +32 -0
- package/lib/typescript/module/hooks/useGutters.d.ts.map +1 -0
- package/lib/typescript/module/hooks/useHaptics.d.ts +28 -0
- package/lib/typescript/module/hooks/useHaptics.d.ts.map +1 -0
- package/lib/typescript/module/hooks/useInteractionState.d.ts +6 -0
- package/lib/typescript/module/hooks/useInteractionState.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +16 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +16 -0
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/media-inset-border/MediaInsetBorder.d.ts +26 -0
- package/lib/typescript/module/media-inset-border/MediaInsetBorder.d.ts.map +1 -0
- package/lib/typescript/module/media-inset-border/index.d.ts +3 -0
- package/lib/typescript/module/media-inset-border/index.d.ts.map +1 -0
- package/lib/typescript/module/motion/ScreenTransition.d.ts +33 -0
- package/lib/typescript/module/motion/ScreenTransition.d.ts.map +1 -0
- package/lib/typescript/module/motion/index.d.ts +4 -0
- package/lib/typescript/module/motion/index.d.ts.map +1 -0
- package/lib/typescript/module/motion/motion.d.ts +24 -0
- package/lib/typescript/module/motion/motion.d.ts.map +1 -0
- package/lib/typescript/module/pressable-scale/PressableScale.d.ts +21 -0
- package/lib/typescript/module/pressable-scale/PressableScale.d.ts.map +1 -0
- package/lib/typescript/module/pressable-scale/index.d.ts +3 -0
- package/lib/typescript/module/pressable-scale/index.d.ts.map +1 -0
- package/lib/typescript/module/pressable-with-hover/PressableWithHover.d.ts +18 -0
- package/lib/typescript/module/pressable-with-hover/PressableWithHover.d.ts.map +1 -0
- package/lib/typescript/module/pressable-with-hover/index.d.ts +3 -0
- package/lib/typescript/module/pressable-with-hover/index.d.ts.map +1 -0
- package/lib/typescript/module/styles/breakpoints.d.ts +18 -0
- package/lib/typescript/module/styles/breakpoints.d.ts.map +1 -0
- package/lib/typescript/module/styles/index.d.ts +2 -0
- package/lib/typescript/module/styles/index.d.ts.map +1 -1
- package/lib/typescript/module/subtle-hover/SubtleHover.d.ts +32 -0
- package/lib/typescript/module/subtle-hover/SubtleHover.d.ts.map +1 -0
- package/lib/typescript/module/subtle-hover/index.d.ts +3 -0
- package/lib/typescript/module/subtle-hover/index.d.ts.map +1 -0
- package/lib/typescript/module/theme/build-theme.d.ts.map +1 -1
- package/lib/typescript/module/theme/gradients.d.ts +12 -0
- package/lib/typescript/module/theme/gradients.d.ts.map +1 -0
- package/lib/typescript/module/theme/index.d.ts +2 -1
- package/lib/typescript/module/theme/index.d.ts.map +1 -1
- package/lib/typescript/module/theme/types.d.ts +11 -0
- package/lib/typescript/module/theme/types.d.ts.map +1 -1
- package/package.json +72 -1
- package/src/__tests__/AnimatedCheck.test.tsx +31 -0
- package/src/__tests__/MediaInsetBorder.test.tsx +36 -0
- package/src/__tests__/Motion.test.tsx +58 -0
- package/src/__tests__/PressableScale.test.tsx +29 -0
- package/src/__tests__/PressableWithHover.test.tsx +48 -0
- package/src/__tests__/SubtleHover.test.tsx +42 -0
- package/src/__tests__/theme-gradients.test.ts +31 -0
- package/src/__tests__/theme.test.ts +3 -0
- package/src/__tests__/useGutters.test.ts +58 -0
- package/src/__tests__/useHaptics.test.tsx +58 -0
- package/src/__tests__/useInteractionState.test.ts +27 -0
- package/src/animated-check/AnimatedCheck.stories.tsx +60 -0
- package/src/animated-check/AnimatedCheck.tsx +173 -0
- package/src/animated-check/index.ts +2 -0
- package/src/dialog/placement.ts +10 -7
- package/src/fill/index.tsx +2 -2
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useGutters.ts +99 -0
- package/src/hooks/useHaptics.tsx +69 -0
- package/src/hooks/useInteractionState.ts +8 -2
- package/src/index.ts +20 -0
- package/src/index.web.ts +20 -0
- package/src/media-inset-border/MediaInsetBorder.stories.tsx +60 -0
- package/src/media-inset-border/MediaInsetBorder.tsx +72 -0
- package/src/media-inset-border/index.ts +2 -0
- package/src/motion/Motion.stories.tsx +99 -0
- package/src/motion/ScreenTransition.tsx +71 -0
- package/src/motion/index.ts +3 -0
- package/src/motion/motion.ts +75 -0
- package/src/pressable-scale/PressableScale.stories.tsx +71 -0
- package/src/pressable-scale/PressableScale.tsx +103 -0
- package/src/pressable-scale/index.ts +2 -0
- package/src/pressable-with-hover/PressableWithHover.stories.tsx +43 -0
- package/src/pressable-with-hover/PressableWithHover.tsx +43 -0
- package/src/pressable-with-hover/index.ts +2 -0
- package/src/styles/breakpoints.ts +18 -0
- package/src/styles/index.ts +2 -0
- package/src/subtle-hover/SubtleHover.stories.tsx +72 -0
- package/src/subtle-hover/SubtleHover.tsx +90 -0
- package/src/subtle-hover/index.ts +2 -0
- package/src/theme/build-theme.ts +2 -0
- package/src/theme/gradients.ts +61 -0
- package/src/theme/index.ts +2 -1
- package/src/theme/types.ts +13 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback, useImperativeHandle, useMemo } from 'react';
|
|
2
|
+
import { StyleSheet, Text } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
Easing,
|
|
5
|
+
useAnimatedProps,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
withDelay,
|
|
8
|
+
withTiming,
|
|
9
|
+
} from 'react-native-reanimated';
|
|
10
|
+
|
|
11
|
+
import { getSvgModule } from '../avatar/svg-module';
|
|
12
|
+
import { useTheme } from '../theme';
|
|
13
|
+
|
|
14
|
+
/** The resolved `react-native-svg` module (non-null branch). */
|
|
15
|
+
type SvgModule = NonNullable<ReturnType<typeof getSvgModule>>;
|
|
16
|
+
|
|
17
|
+
/** Square viewBox the ring + check are drawn on. */
|
|
18
|
+
const VIEW_BOX = 52;
|
|
19
|
+
/** Ring radius within the viewBox. */
|
|
20
|
+
const CIRCLE_RADIUS = 24;
|
|
21
|
+
/** Ring circumference (2·π·r ≈ 150.8) rounded up so the dash fully hides it. */
|
|
22
|
+
const CIRCLE_LENGTH = 166;
|
|
23
|
+
/** Approximate path length of the check stroke. */
|
|
24
|
+
const CHECK_LENGTH = 48;
|
|
25
|
+
/** The check glyph, drawn on the 52×52 canvas. */
|
|
26
|
+
const CHECK_PATH = 'M14.1 27.2l7.1 7.2 16.7-16.8';
|
|
27
|
+
const STROKE_WIDTH = 4;
|
|
28
|
+
/** Ring draw-on duration (ms). */
|
|
29
|
+
const CIRCLE_DURATION = 500;
|
|
30
|
+
/** Check draw-on duration (ms), started once the ring completes. */
|
|
31
|
+
const CHECK_DURATION = 300;
|
|
32
|
+
|
|
33
|
+
export interface AnimatedCheckRef {
|
|
34
|
+
/** Replays the draw-on animation from the start. */
|
|
35
|
+
play: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AnimatedCheckProps {
|
|
39
|
+
/** Width and height in px. Defaults to `24`. */
|
|
40
|
+
size?: number;
|
|
41
|
+
/** Stroke color of the ring and check. Defaults to the theme success color. */
|
|
42
|
+
color?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// `Animated.createAnimatedComponent` must be called once per underlying
|
|
46
|
+
// component (calling it per-render would remount and break the animation).
|
|
47
|
+
// react-native-svg is a lazy/optional peer, so we build the animated wrappers
|
|
48
|
+
// the first time it resolves and cache them at module scope.
|
|
49
|
+
function buildAnimatedSvg(svg: SvgModule) {
|
|
50
|
+
return {
|
|
51
|
+
Circle: Animated.createAnimatedComponent(svg.Circle),
|
|
52
|
+
Path: Animated.createAnimatedComponent(svg.Path),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let animatedSvgCache: ReturnType<typeof buildAnimatedSvg> | null = null;
|
|
57
|
+
|
|
58
|
+
function getAnimatedSvg(svg: SvgModule): ReturnType<typeof buildAnimatedSvg> {
|
|
59
|
+
if (!animatedSvgCache) animatedSvgCache = buildAnimatedSvg(svg);
|
|
60
|
+
return animatedSvgCache;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface AnimatedCheckSvgProps {
|
|
64
|
+
size: number;
|
|
65
|
+
color: string;
|
|
66
|
+
svg: SvgModule;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The animated SVG implementation, rendered only when react-native-svg exists. */
|
|
70
|
+
const AnimatedCheckSvg = forwardRef<AnimatedCheckRef, AnimatedCheckSvgProps>(
|
|
71
|
+
function AnimatedCheckSvg({ size, color, svg }, ref) {
|
|
72
|
+
const { default: Svg } = svg;
|
|
73
|
+
const { Circle, Path } = getAnimatedSvg(svg);
|
|
74
|
+
|
|
75
|
+
const circleProgress = useSharedValue(0);
|
|
76
|
+
const checkProgress = useSharedValue(0);
|
|
77
|
+
|
|
78
|
+
const circleAnimatedProps = useAnimatedProps(() => ({
|
|
79
|
+
strokeDashoffset: CIRCLE_LENGTH - circleProgress.value * CIRCLE_LENGTH,
|
|
80
|
+
}));
|
|
81
|
+
const checkAnimatedProps = useAnimatedProps(() => ({
|
|
82
|
+
strokeDashoffset: CHECK_LENGTH - CHECK_LENGTH * checkProgress.value,
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
const play = useCallback(() => {
|
|
86
|
+
circleProgress.value = 0;
|
|
87
|
+
checkProgress.value = 0;
|
|
88
|
+
circleProgress.value = withTiming(1, {
|
|
89
|
+
duration: CIRCLE_DURATION,
|
|
90
|
+
easing: Easing.linear,
|
|
91
|
+
});
|
|
92
|
+
checkProgress.value = withDelay(
|
|
93
|
+
CIRCLE_DURATION,
|
|
94
|
+
withTiming(1, { duration: CHECK_DURATION, easing: Easing.linear }),
|
|
95
|
+
);
|
|
96
|
+
}, [circleProgress, checkProgress]);
|
|
97
|
+
|
|
98
|
+
useImperativeHandle(ref, () => ({ play }), [play]);
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<Svg fill="none" viewBox={`0 0 ${VIEW_BOX} ${VIEW_BOX}`} width={size} height={size}>
|
|
102
|
+
<Circle
|
|
103
|
+
animatedProps={circleAnimatedProps}
|
|
104
|
+
cx={VIEW_BOX / 2}
|
|
105
|
+
cy={VIEW_BOX / 2}
|
|
106
|
+
r={CIRCLE_RADIUS}
|
|
107
|
+
fill="none"
|
|
108
|
+
stroke={color}
|
|
109
|
+
strokeWidth={STROKE_WIDTH}
|
|
110
|
+
strokeDasharray={CIRCLE_LENGTH}
|
|
111
|
+
/>
|
|
112
|
+
<Path
|
|
113
|
+
animatedProps={checkAnimatedProps}
|
|
114
|
+
d={CHECK_PATH}
|
|
115
|
+
stroke={color}
|
|
116
|
+
strokeWidth={STROKE_WIDTH}
|
|
117
|
+
strokeDasharray={CHECK_LENGTH}
|
|
118
|
+
/>
|
|
119
|
+
</Svg>
|
|
120
|
+
);
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
interface StaticCheckProps {
|
|
125
|
+
size: number;
|
|
126
|
+
color: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Static fallback rendered when react-native-svg is unavailable (e.g. a web
|
|
131
|
+
* bundle that omits the optional peer). Draws a plain Unicode check so the
|
|
132
|
+
* component still communicates "done"; `play()` is a no-op.
|
|
133
|
+
*/
|
|
134
|
+
const StaticCheck = forwardRef<AnimatedCheckRef, StaticCheckProps>(
|
|
135
|
+
function StaticCheck({ size, color }, ref) {
|
|
136
|
+
useImperativeHandle(ref, () => ({ play: () => undefined }), []);
|
|
137
|
+
return (
|
|
138
|
+
<Text
|
|
139
|
+
accessibilityElementsHidden
|
|
140
|
+
style={[styles.staticCheck, { width: size, fontSize: size * 0.8, lineHeight: size, color }]}
|
|
141
|
+
>
|
|
142
|
+
{'✓'}
|
|
143
|
+
</Text>
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A draw-on checkmark: a ring strokes on, then the check strokes in. Expose a
|
|
150
|
+
* ref and call `ref.current?.play()` to (re)play — e.g. on a successful submit.
|
|
151
|
+
* Ported from Bluesky's `AnimatedCheck`.
|
|
152
|
+
*/
|
|
153
|
+
export const AnimatedCheck = forwardRef<AnimatedCheckRef, AnimatedCheckProps>(
|
|
154
|
+
function AnimatedCheck({ size = 24, color }, ref) {
|
|
155
|
+
const theme = useTheme();
|
|
156
|
+
const resolvedColor = color ?? theme.colors.success;
|
|
157
|
+
const svg = useMemo(() => getSvgModule(), []);
|
|
158
|
+
|
|
159
|
+
if (!svg) {
|
|
160
|
+
return <StaticCheck ref={ref} size={size} color={resolvedColor} />;
|
|
161
|
+
}
|
|
162
|
+
return <AnimatedCheckSvg ref={ref} size={size} color={resolvedColor} svg={svg} />;
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
AnimatedCheck.displayName = 'AnimatedCheck';
|
|
167
|
+
|
|
168
|
+
const styles = StyleSheet.create({
|
|
169
|
+
staticCheck: {
|
|
170
|
+
textAlign: 'center',
|
|
171
|
+
fontWeight: '700',
|
|
172
|
+
},
|
|
173
|
+
});
|
package/src/dialog/placement.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import { useWindowDimensions } from 'react-native';
|
|
3
3
|
|
|
4
|
+
import { BREAKPOINTS } from '../styles/breakpoints';
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Anchor a `<Dialog>` can render against.
|
|
6
8
|
*
|
|
@@ -31,14 +33,15 @@ export type ResponsiveDialogPlacement =
|
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
|
-
* Tailwind-default breakpoints (px)
|
|
35
|
-
*
|
|
36
|
-
* width
|
|
36
|
+
* Tailwind-default breakpoints (px), re-exported from the canonical
|
|
37
|
+
* {@link BREAKPOINTS} source. A responsive `placement` map resolves to the value
|
|
38
|
+
* for the largest key whose min-width is `<=` the current viewport width,
|
|
39
|
+
* falling back to `base`.
|
|
37
40
|
*/
|
|
38
|
-
export const BREAKPOINT_SM =
|
|
39
|
-
export const BREAKPOINT_MD =
|
|
40
|
-
export const BREAKPOINT_LG =
|
|
41
|
-
export const BREAKPOINT_XL =
|
|
41
|
+
export const BREAKPOINT_SM = BREAKPOINTS.sm;
|
|
42
|
+
export const BREAKPOINT_MD = BREAKPOINTS.md;
|
|
43
|
+
export const BREAKPOINT_LG = BREAKPOINTS.lg;
|
|
44
|
+
export const BREAKPOINT_XL = BREAKPOINTS.xl;
|
|
42
45
|
|
|
43
46
|
/** Ordered breakpoint table, widest first, so the first match wins. */
|
|
44
47
|
const RESPONSIVE_KEYS = [
|
package/src/fill/index.tsx
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import React, { memo } from 'react';
|
|
2
|
-
import { StyleSheet, View, type ViewStyle } from 'react-native';
|
|
2
|
+
import { StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
|
|
4
4
|
const FillComponent = function Fill({
|
|
5
5
|
children,
|
|
6
6
|
style,
|
|
7
7
|
}: {
|
|
8
8
|
children?: React.ReactNode;
|
|
9
|
-
style?: ViewStyle
|
|
9
|
+
style?: StyleProp<ViewStyle>;
|
|
10
10
|
}) {
|
|
11
11
|
return (
|
|
12
12
|
<View style={[fillStyles.base, style]}>
|
package/src/hooks/index.ts
CHANGED
|
@@ -3,3 +3,7 @@ export { useDelayedLoading } from './useDelayedLoading';
|
|
|
3
3
|
export { useThrottledValue } from './useThrottledValue';
|
|
4
4
|
export { usePressAnimation } from './usePressAnimation';
|
|
5
5
|
export { mergeRefs } from './mergeRefs';
|
|
6
|
+
export { useHaptics, BloomHapticsProvider } from './useHaptics';
|
|
7
|
+
export type { HapticStrength, BloomHapticsProviderProps } from './useHaptics';
|
|
8
|
+
export { useGutters } from './useGutters';
|
|
9
|
+
export type { Gutter, Gutters } from './useGutters';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useWindowDimensions } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { BREAKPOINTS } from '../styles/breakpoints';
|
|
5
|
+
import { space } from '../styles/tokens';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A named gutter size, or `0` for no padding on that edge.
|
|
9
|
+
*
|
|
10
|
+
* - `'compact'` — tight gutter (dense lists, chips).
|
|
11
|
+
* - `'base'` — the default page gutter.
|
|
12
|
+
* - `'wide'` — roomy gutter (marketing / hero surfaces).
|
|
13
|
+
*/
|
|
14
|
+
export type Gutter = 'compact' | 'base' | 'wide' | 0;
|
|
15
|
+
|
|
16
|
+
/** Resolved edge padding in px — one value per side, ready to spread onto a style. */
|
|
17
|
+
export interface Gutters {
|
|
18
|
+
paddingTop: number;
|
|
19
|
+
paddingRight: number;
|
|
20
|
+
paddingBottom: number;
|
|
21
|
+
paddingLeft: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The active viewport tier: the widest matched breakpoint, or `'base'` below `sm`. */
|
|
25
|
+
type GutterTier = 'base' | 'sm' | 'md' | 'lg' | 'xl';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Per-size gutter padding (px) at each viewport tier. Padding steps up around
|
|
29
|
+
* tablet width (`md`), mirroring the design-system scale: phones get a tighter
|
|
30
|
+
* gutter, wider screens a roomier one. Values come from the shared `space`
|
|
31
|
+
* tokens so gutters stay on the spacing grid.
|
|
32
|
+
*/
|
|
33
|
+
const GUTTER_SCALE: Record<Exclude<Gutter, 0>, Record<GutterTier, number>> = {
|
|
34
|
+
compact: { base: space.sm, sm: space.sm, md: space.md, lg: space.md, xl: space.md },
|
|
35
|
+
base: { base: space.lg, sm: space.lg, md: space.xl, lg: space.xl, xl: space.xl },
|
|
36
|
+
wide: { base: space.xl, sm: space.xl, md: space._3xl, lg: space._3xl, xl: space._3xl },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Widest-first breakpoint table so the first match wins. */
|
|
40
|
+
const TIERS: ReadonlyArray<readonly [Exclude<GutterTier, 'base'>, number]> = [
|
|
41
|
+
['xl', BREAKPOINTS.xl],
|
|
42
|
+
['lg', BREAKPOINTS.lg],
|
|
43
|
+
['md', BREAKPOINTS.md],
|
|
44
|
+
['sm', BREAKPOINTS.sm],
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function resolveTier(width: number): GutterTier {
|
|
48
|
+
for (const [tier, minWidth] of TIERS) {
|
|
49
|
+
if (width >= minWidth) return tier;
|
|
50
|
+
}
|
|
51
|
+
return 'base';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveEdge(size: Gutter, tier: GutterTier): number {
|
|
55
|
+
return size === 0 ? 0 : GUTTER_SCALE[size][tier];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Expand a 1/2/4-arg CSS-shorthand gutter list into `[top, right, bottom, left]`.
|
|
60
|
+
* `??` (not `||`) so an explicit `0` is preserved rather than falling through.
|
|
61
|
+
*/
|
|
62
|
+
function normalizeShorthand(gutters: Gutter[]): [Gutter, Gutter, Gutter, Gutter] {
|
|
63
|
+
const top = gutters[0] ?? 0;
|
|
64
|
+
const right = gutters[1] ?? top;
|
|
65
|
+
const bottom = gutters[2] ?? top;
|
|
66
|
+
const left = gutters[3] ?? right;
|
|
67
|
+
return [top, right, bottom, left];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Breakpoint-aware gutter padding. Give it CSS-shorthand gutter sizes and it
|
|
72
|
+
* returns resolved `padding*` values for the current viewport width, recomputed
|
|
73
|
+
* as the window crosses a breakpoint.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```tsx
|
|
77
|
+
* const gutters = useGutters(['base']); // uniform
|
|
78
|
+
* const gutters = useGutters([0, 'wide']); // no vertical, wide horizontal
|
|
79
|
+
* const gutters = useGutters(['compact', 'base', 0, 'base']); // t / r / b / l
|
|
80
|
+
* <View style={gutters} />
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function useGutters(gutters: [Gutter]): Gutters;
|
|
84
|
+
export function useGutters(gutters: [Gutter, Gutter]): Gutters;
|
|
85
|
+
export function useGutters(gutters: [Gutter, Gutter, Gutter, Gutter]): Gutters;
|
|
86
|
+
export function useGutters(gutters: Gutter[]): Gutters {
|
|
87
|
+
const { width } = useWindowDimensions();
|
|
88
|
+
const [top, right, bottom, left] = normalizeShorthand(gutters);
|
|
89
|
+
|
|
90
|
+
return useMemo<Gutters>(() => {
|
|
91
|
+
const tier = resolveTier(width);
|
|
92
|
+
return {
|
|
93
|
+
paddingTop: resolveEdge(top, tier),
|
|
94
|
+
paddingRight: resolveEdge(right, tier),
|
|
95
|
+
paddingBottom: resolveEdge(bottom, tier),
|
|
96
|
+
paddingLeft: resolveEdge(left, tier),
|
|
97
|
+
};
|
|
98
|
+
}, [width, top, right, bottom, left]);
|
|
99
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { createContext, useCallback, useContext, useMemo } from 'react';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { lazyRequire } from '../utils/lazy-require';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Lazily loads `expo-haptics`, returning `null` when it is not installed. The
|
|
8
|
+
* dependency is an optional peer, so calls degrade to a no-op when absent.
|
|
9
|
+
*/
|
|
10
|
+
const getHapticsModule = lazyRequire<typeof import('expo-haptics')>('expo-haptics');
|
|
11
|
+
|
|
12
|
+
/** Impact strength requested by a caller. */
|
|
13
|
+
export type HapticStrength = 'light' | 'medium' | 'heavy';
|
|
14
|
+
|
|
15
|
+
interface BloomHapticsContextValue {
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const BloomHapticsContext = createContext<BloomHapticsContextValue>({ enabled: true });
|
|
20
|
+
|
|
21
|
+
export interface BloomHapticsProviderProps {
|
|
22
|
+
/** Globally enable or disable haptics for the subtree. Defaults to `true`. */
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
children?: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Optional provider that toggles haptic feedback for its subtree — e.g. to honour
|
|
29
|
+
* a user "reduce haptics" preference. `useHaptics` defaults to enabled when no
|
|
30
|
+
* provider is present.
|
|
31
|
+
*/
|
|
32
|
+
export function BloomHapticsProvider({ enabled = true, children }: BloomHapticsProviderProps) {
|
|
33
|
+
const value = useMemo(() => ({ enabled }), [enabled]);
|
|
34
|
+
return <BloomHapticsContext.Provider value={value}>{children}</BloomHapticsContext.Provider>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
BloomHapticsProvider.displayName = 'BloomHapticsProvider';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns a function that fires a haptic impact. The returned callback no-ops on
|
|
41
|
+
* web, when haptics are disabled via {@link BloomHapticsProvider}, or when the
|
|
42
|
+
* optional `expo-haptics` peer is not installed.
|
|
43
|
+
*
|
|
44
|
+
* `strength` defaults to `'light'`. Android is clamped to the light impact style
|
|
45
|
+
* regardless of `strength` — its medium/heavy motors read as too strong for UI
|
|
46
|
+
* feedback (tuned to match iOS perceived intensity).
|
|
47
|
+
*/
|
|
48
|
+
export function useHaptics(): (strength?: HapticStrength) => void {
|
|
49
|
+
const { enabled } = useContext(BloomHapticsContext);
|
|
50
|
+
|
|
51
|
+
return useCallback(
|
|
52
|
+
(strength: HapticStrength = 'light') => {
|
|
53
|
+
if (!enabled || Platform.OS === 'web') return;
|
|
54
|
+
|
|
55
|
+
const haptics = getHapticsModule();
|
|
56
|
+
if (!haptics) return;
|
|
57
|
+
|
|
58
|
+
const { ImpactFeedbackStyle } = haptics;
|
|
59
|
+
let style = ImpactFeedbackStyle.Light;
|
|
60
|
+
if (Platform.OS !== 'android') {
|
|
61
|
+
if (strength === 'medium') style = ImpactFeedbackStyle.Medium;
|
|
62
|
+
else if (strength === 'heavy') style = ImpactFeedbackStyle.Heavy;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
void haptics.impactAsync(style);
|
|
66
|
+
},
|
|
67
|
+
[enabled],
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import { useCallback, useState } from 'react';
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Tracks a single boolean interaction flag (hovered / focused / pressed).
|
|
5
|
+
*
|
|
6
|
+
* Returns a memoized `{ state, onIn, onOut }` with stable handler identities,
|
|
7
|
+
* suitable for spreading onto Pressable-like components.
|
|
8
|
+
*/
|
|
3
9
|
export function useInteractionState() {
|
|
4
10
|
const [state, setState] = useState(false);
|
|
5
11
|
|
|
@@ -10,7 +16,7 @@ export function useInteractionState() {
|
|
|
10
16
|
setState(false);
|
|
11
17
|
}, []);
|
|
12
18
|
|
|
13
|
-
return { state, onIn, onOut };
|
|
19
|
+
return useMemo(() => ({ state, onIn, onOut }), [state, onIn, onOut]);
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
/**
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,10 @@ export type {
|
|
|
40
40
|
export { useInteractionState } from './hooks/useInteractionState';
|
|
41
41
|
export { useDelayedLoading } from './hooks/useDelayedLoading';
|
|
42
42
|
export { useThrottledValue } from './hooks/useThrottledValue';
|
|
43
|
+
export { useHaptics, BloomHapticsProvider } from './hooks/useHaptics';
|
|
44
|
+
export type { HapticStrength, BloomHapticsProviderProps } from './hooks/useHaptics';
|
|
45
|
+
export { useGutters } from './hooks/useGutters';
|
|
46
|
+
export type { Gutter, Gutters } from './hooks/useGutters';
|
|
43
47
|
|
|
44
48
|
// Icons
|
|
45
49
|
export * as Icons from './icons';
|
|
@@ -97,12 +101,28 @@ export * as Typography from './typography';
|
|
|
97
101
|
export * as Skeleton from './skeleton';
|
|
98
102
|
export * as Grid from './grid';
|
|
99
103
|
export { Fill } from './fill';
|
|
104
|
+
export { MediaInsetBorder } from './media-inset-border';
|
|
105
|
+
export type { MediaInsetBorderProps } from './media-inset-border';
|
|
100
106
|
export { IconCircle } from './icon-circle';
|
|
101
107
|
export { ConnectionDots } from './connection-dots';
|
|
102
108
|
export type { ConnectionDotsProps } from './connection-dots';
|
|
103
109
|
export { BenefitRow, BenefitList } from './benefit-list';
|
|
104
110
|
export type { BenefitRowProps, BenefitListProps } from './benefit-list';
|
|
105
111
|
|
|
112
|
+
// Interaction primitives
|
|
113
|
+
export { PressableScale } from './pressable-scale';
|
|
114
|
+
export type { PressableScaleProps } from './pressable-scale';
|
|
115
|
+
export { PressableWithHover } from './pressable-with-hover';
|
|
116
|
+
export type { PressableWithHoverProps } from './pressable-with-hover';
|
|
117
|
+
export { SubtleHover } from './subtle-hover';
|
|
118
|
+
export type { SubtleHoverProps } from './subtle-hover';
|
|
119
|
+
|
|
120
|
+
// Motion presets (Reanimated enter/exit + directional screen transition)
|
|
121
|
+
export { ScaleAndFadeIn, ScaleAndFadeOut, ShrinkAndPop, ScreenTransition } from './motion';
|
|
122
|
+
export type { ScreenTransitionProps, ScreenTransitionDirection } from './motion';
|
|
123
|
+
export { AnimatedCheck } from './animated-check';
|
|
124
|
+
export type { AnimatedCheckProps, AnimatedCheckRef } from './animated-check';
|
|
125
|
+
|
|
106
126
|
// Form components
|
|
107
127
|
export * from './text-field';
|
|
108
128
|
export * from './segmented-control';
|
package/src/index.web.ts
CHANGED
|
@@ -45,6 +45,10 @@ export type {
|
|
|
45
45
|
export { useInteractionState } from './hooks/useInteractionState';
|
|
46
46
|
export { useDelayedLoading } from './hooks/useDelayedLoading';
|
|
47
47
|
export { useThrottledValue } from './hooks/useThrottledValue';
|
|
48
|
+
export { useHaptics, BloomHapticsProvider } from './hooks/useHaptics';
|
|
49
|
+
export type { HapticStrength, BloomHapticsProviderProps } from './hooks/useHaptics';
|
|
50
|
+
export { useGutters } from './hooks/useGutters';
|
|
51
|
+
export type { Gutter, Gutters } from './hooks/useGutters';
|
|
48
52
|
|
|
49
53
|
// Icons
|
|
50
54
|
export * as Icons from './icons';
|
|
@@ -102,12 +106,28 @@ export * as Typography from './typography';
|
|
|
102
106
|
export * as Skeleton from './skeleton';
|
|
103
107
|
export * as Grid from './grid';
|
|
104
108
|
export { Fill } from './fill';
|
|
109
|
+
export { MediaInsetBorder } from './media-inset-border';
|
|
110
|
+
export type { MediaInsetBorderProps } from './media-inset-border';
|
|
105
111
|
export { IconCircle } from './icon-circle';
|
|
106
112
|
export { ConnectionDots } from './connection-dots/index.web';
|
|
107
113
|
export type { ConnectionDotsProps } from './connection-dots/index.web';
|
|
108
114
|
export { BenefitRow, BenefitList } from './benefit-list';
|
|
109
115
|
export type { BenefitRowProps, BenefitListProps } from './benefit-list';
|
|
110
116
|
|
|
117
|
+
// Interaction primitives
|
|
118
|
+
export { PressableScale } from './pressable-scale';
|
|
119
|
+
export type { PressableScaleProps } from './pressable-scale';
|
|
120
|
+
export { PressableWithHover } from './pressable-with-hover';
|
|
121
|
+
export type { PressableWithHoverProps } from './pressable-with-hover';
|
|
122
|
+
export { SubtleHover } from './subtle-hover';
|
|
123
|
+
export type { SubtleHoverProps } from './subtle-hover';
|
|
124
|
+
|
|
125
|
+
// Motion presets (Reanimated enter/exit + directional screen transition)
|
|
126
|
+
export { ScaleAndFadeIn, ScaleAndFadeOut, ShrinkAndPop, ScreenTransition } from './motion';
|
|
127
|
+
export type { ScreenTransitionProps, ScreenTransitionDirection } from './motion';
|
|
128
|
+
export { AnimatedCheck } from './animated-check';
|
|
129
|
+
export type { AnimatedCheckProps, AnimatedCheckRef } from './animated-check';
|
|
130
|
+
|
|
111
131
|
// Form components
|
|
112
132
|
export * from './text-field';
|
|
113
133
|
export * from './segmented-control';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Image, View } from 'react-native';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
|
+
|
|
5
|
+
import { MediaInsetBorder } from './index';
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof MediaInsetBorder> = {
|
|
8
|
+
title: 'Components/MediaInsetBorder',
|
|
9
|
+
component: MediaInsetBorder,
|
|
10
|
+
argTypes: {
|
|
11
|
+
opaque: { control: 'boolean' },
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
|
|
17
|
+
type Story = StoryObj<typeof MediaInsetBorder>;
|
|
18
|
+
|
|
19
|
+
const SAMPLE_URI =
|
|
20
|
+
'https://images.unsplash.com/photo-1526779259212-939e64788e3c?w=400&q=80';
|
|
21
|
+
|
|
22
|
+
export const OverImage: Story = {
|
|
23
|
+
render: (args) => (
|
|
24
|
+
<View style={{ width: 240, height: 160, borderRadius: 8, overflow: 'hidden' }}>
|
|
25
|
+
<Image source={{ uri: SAMPLE_URI }} style={{ width: '100%', height: '100%' }} />
|
|
26
|
+
<MediaInsetBorder {...args} />
|
|
27
|
+
</View>
|
|
28
|
+
),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const OverFlatSurface: Story = {
|
|
32
|
+
render: (args) => (
|
|
33
|
+
<View
|
|
34
|
+
style={{
|
|
35
|
+
width: 240,
|
|
36
|
+
height: 160,
|
|
37
|
+
borderRadius: 8,
|
|
38
|
+
backgroundColor: '#e5e7eb',
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<MediaInsetBorder {...args} />
|
|
42
|
+
</View>
|
|
43
|
+
),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Opaque: Story = {
|
|
47
|
+
args: { opaque: true },
|
|
48
|
+
render: (args) => (
|
|
49
|
+
<View
|
|
50
|
+
style={{
|
|
51
|
+
width: 240,
|
|
52
|
+
height: 160,
|
|
53
|
+
borderRadius: 8,
|
|
54
|
+
backgroundColor: '#e5e7eb',
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<MediaInsetBorder {...args} />
|
|
58
|
+
</View>
|
|
59
|
+
),
|
|
60
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
PixelRatio,
|
|
4
|
+
Platform,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type ViewStyle,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
|
|
10
|
+
import { Fill } from '../fill';
|
|
11
|
+
import { borderRadius } from '../styles/tokens';
|
|
12
|
+
import { useTheme } from '../theme';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A 1px (hairline) inset border reads too heavy on high-DPI web displays, so we
|
|
16
|
+
* drop to 0.5px there. On native the platform hairline is already correct, and
|
|
17
|
+
* `PixelRatio` is only consulted on web.
|
|
18
|
+
*/
|
|
19
|
+
const HAIRLINE_WIDTH =
|
|
20
|
+
Platform.OS === 'web' && PixelRatio.get() > 1 ? 0.5 : StyleSheet.hairlineWidth;
|
|
21
|
+
|
|
22
|
+
export interface MediaInsetBorderProps {
|
|
23
|
+
/**
|
|
24
|
+
* Render a solid, full-contrast border instead of the default softened one.
|
|
25
|
+
* Use where the inset border must line up with adjacent opaque borders, such
|
|
26
|
+
* as external link-preview cards.
|
|
27
|
+
*/
|
|
28
|
+
opaque?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Extra style merged last. Override `borderRadius` here to match the corner
|
|
31
|
+
* radius of the surrounding media box.
|
|
32
|
+
*/
|
|
33
|
+
style?: StyleProp<ViewStyle>;
|
|
34
|
+
children?: React.ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A theme-aware hairline border painted just inside a media box (image, avatar,
|
|
39
|
+
* embed) to separate it from the surrounding background. Absolutely positioned
|
|
40
|
+
* to fill its parent (built on {@link Fill}) and non-interactive.
|
|
41
|
+
*/
|
|
42
|
+
export function MediaInsetBorder({ opaque, style, children }: MediaInsetBorderProps) {
|
|
43
|
+
const theme = useTheme();
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Fill
|
|
47
|
+
style={[
|
|
48
|
+
styles.base,
|
|
49
|
+
{ borderColor: theme.colors.border },
|
|
50
|
+
!opaque && styles.soft,
|
|
51
|
+
style,
|
|
52
|
+
]}
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
</Fill>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
MediaInsetBorder.displayName = 'MediaInsetBorder';
|
|
60
|
+
|
|
61
|
+
const styles = StyleSheet.create({
|
|
62
|
+
base: {
|
|
63
|
+
borderRadius: borderRadius.sm,
|
|
64
|
+
borderWidth: HAIRLINE_WIDTH,
|
|
65
|
+
pointerEvents: 'none',
|
|
66
|
+
},
|
|
67
|
+
// Non-opaque: same border color at reduced opacity so it reads as a subtle,
|
|
68
|
+
// lower-contrast separator rather than a hard edge.
|
|
69
|
+
soft: {
|
|
70
|
+
opacity: 0.6,
|
|
71
|
+
},
|
|
72
|
+
});
|