@oxyhq/bloom 0.26.1 → 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/avatar/Avatar.js +67 -96
- package/lib/commonjs/avatar/Avatar.js.map +1 -1
- package/lib/commonjs/avatar/AvatarRing.js +183 -0
- package/lib/commonjs/avatar/AvatarRing.js.map +1 -0
- package/lib/commonjs/avatar/index.js.map +1 -1
- package/lib/commonjs/avatar/squircle-path.js +14 -0
- package/lib/commonjs/avatar/squircle-path.js.map +1 -0
- package/lib/commonjs/avatar/svg-module.js +15 -0
- package/lib/commonjs/avatar/svg-module.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/avatar/Avatar.js +66 -96
- package/lib/module/avatar/Avatar.js.map +1 -1
- package/lib/module/avatar/AvatarRing.js +177 -0
- package/lib/module/avatar/AvatarRing.js.map +1 -0
- package/lib/module/avatar/index.js.map +1 -1
- package/lib/module/avatar/squircle-path.js +10 -0
- package/lib/module/avatar/squircle-path.js.map +1 -0
- package/lib/module/avatar/svg-module.js +11 -0
- package/lib/module/avatar/svg-module.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/avatar/Avatar.d.ts.map +1 -1
- package/lib/typescript/commonjs/avatar/AvatarRing.d.ts +27 -0
- package/lib/typescript/commonjs/avatar/AvatarRing.d.ts.map +1 -0
- package/lib/typescript/commonjs/avatar/index.d.ts +1 -1
- package/lib/typescript/commonjs/avatar/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/avatar/squircle-path.d.ts +8 -0
- package/lib/typescript/commonjs/avatar/squircle-path.d.ts.map +1 -0
- package/lib/typescript/commonjs/avatar/svg-module.d.ts +8 -0
- package/lib/typescript/commonjs/avatar/svg-module.d.ts.map +1 -0
- package/lib/typescript/commonjs/avatar/types.d.ts +30 -0
- package/lib/typescript/commonjs/avatar/types.d.ts.map +1 -1
- 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/avatar/Avatar.d.ts.map +1 -1
- package/lib/typescript/module/avatar/AvatarRing.d.ts +27 -0
- package/lib/typescript/module/avatar/AvatarRing.d.ts.map +1 -0
- package/lib/typescript/module/avatar/index.d.ts +1 -1
- package/lib/typescript/module/avatar/index.d.ts.map +1 -1
- package/lib/typescript/module/avatar/squircle-path.d.ts +8 -0
- package/lib/typescript/module/avatar/squircle-path.d.ts.map +1 -0
- package/lib/typescript/module/avatar/svg-module.d.ts +8 -0
- package/lib/typescript/module/avatar/svg-module.d.ts.map +1 -0
- package/lib/typescript/module/avatar/types.d.ts +30 -0
- package/lib/typescript/module/avatar/types.d.ts.map +1 -1
- 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 +73 -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/avatar/Avatar.stories.tsx +95 -0
- package/src/avatar/Avatar.tsx +61 -110
- package/src/avatar/AvatarRing.tsx +170 -0
- package/src/avatar/index.ts +6 -1
- package/src/avatar/squircle-path.ts +11 -0
- package/src/avatar/svg-module.ts +11 -0
- package/src/avatar/types.ts +32 -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,48 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text } from 'react-native';
|
|
3
|
+
import { fireEvent, render } from '@testing-library/react-native';
|
|
4
|
+
|
|
5
|
+
import { PressableWithHover } from '../pressable-with-hover';
|
|
6
|
+
|
|
7
|
+
describe('PressableWithHover', () => {
|
|
8
|
+
it('carries a displayName', () => {
|
|
9
|
+
expect(PressableWithHover.displayName).toBe('PressableWithHover');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders its children', () => {
|
|
13
|
+
const { getByText } = render(
|
|
14
|
+
<PressableWithHover
|
|
15
|
+
accessibilityLabel="Row"
|
|
16
|
+
style={{ backgroundColor: 'white' }}
|
|
17
|
+
hoverStyle={{ backgroundColor: 'gray' }}
|
|
18
|
+
>
|
|
19
|
+
<Text>Row</Text>
|
|
20
|
+
</PressableWithHover>,
|
|
21
|
+
);
|
|
22
|
+
expect(getByText('Row')).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('merges hoverStyle only while hovered', () => {
|
|
26
|
+
const { getByTestId } = render(
|
|
27
|
+
<PressableWithHover
|
|
28
|
+
testID="row"
|
|
29
|
+
style={{ backgroundColor: 'white' }}
|
|
30
|
+
hoverStyle={{ backgroundColor: 'gray' }}
|
|
31
|
+
>
|
|
32
|
+
<Text>Row</Text>
|
|
33
|
+
</PressableWithHover>,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const row = getByTestId('row');
|
|
37
|
+
expect(row.props.style).toEqual({ backgroundColor: 'white' });
|
|
38
|
+
|
|
39
|
+
fireEvent(row, 'hoverIn');
|
|
40
|
+
expect(getByTestId('row').props.style).toEqual([
|
|
41
|
+
{ backgroundColor: 'white' },
|
|
42
|
+
{ backgroundColor: 'gray' },
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
fireEvent(row, 'hoverOut');
|
|
46
|
+
expect(getByTestId('row').props.style).toEqual({ backgroundColor: 'white' });
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react-native';
|
|
3
|
+
|
|
4
|
+
import { BloomThemeProvider } from '../theme/BloomThemeProvider';
|
|
5
|
+
import { SubtleHover } from '../subtle-hover';
|
|
6
|
+
|
|
7
|
+
// Default test Platform.OS is 'ios' (see __mocks__/react-native.ts), so the
|
|
8
|
+
// web-only overlay is suppressed unless `native` is opted in.
|
|
9
|
+
function renderWithTheme(ui: React.ReactElement) {
|
|
10
|
+
return render(
|
|
11
|
+
<BloomThemeProvider mode="light" colorPreset="teal">
|
|
12
|
+
{ui}
|
|
13
|
+
</BloomThemeProvider>,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('SubtleHover', () => {
|
|
18
|
+
it('carries a displayName', () => {
|
|
19
|
+
expect(SubtleHover.displayName).toBe('SubtleHover');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// `renderWithTheme` wraps the subject in the provider's flex View, so the
|
|
23
|
+
// overlay's presence is read from that wrapper's `children` rather than the
|
|
24
|
+
// root being null.
|
|
25
|
+
it('renders nothing on native by default', () => {
|
|
26
|
+
const tree = renderWithTheme(<SubtleHover hover />).toJSON();
|
|
27
|
+
expect(tree).not.toBeNull();
|
|
28
|
+
expect(tree?.children).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('renders the overlay on native when opted in', () => {
|
|
32
|
+
const tree = renderWithTheme(<SubtleHover hover native />).toJSON();
|
|
33
|
+
expect(tree?.children).not.toBeNull();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('accepts an opacity override and a style', () => {
|
|
37
|
+
const tree = renderWithTheme(
|
|
38
|
+
<SubtleHover hover native opacity={0.9} style={{ borderRadius: 12 }} />,
|
|
39
|
+
).toJSON();
|
|
40
|
+
expect(tree?.children).not.toBeNull();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { buildTheme } from '../theme/build-theme';
|
|
2
|
+
import { THEME_GRADIENTS } from '../theme/gradients';
|
|
3
|
+
|
|
4
|
+
describe('theme gradients', () => {
|
|
5
|
+
it('are exposed on the built theme', () => {
|
|
6
|
+
const theme = buildTheme('teal', 'light');
|
|
7
|
+
expect(theme.gradients).toBe(THEME_GRADIENTS);
|
|
8
|
+
expect(Object.keys(theme.gradients).length).toBeGreaterThan(0);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('every gradient has ordered [offset, color] stops', () => {
|
|
12
|
+
for (const [name, gradient] of Object.entries(THEME_GRADIENTS)) {
|
|
13
|
+
expect(name).toBeTruthy();
|
|
14
|
+
expect(Array.isArray(gradient.values)).toBe(true);
|
|
15
|
+
expect(gradient.values.length).toBeGreaterThanOrEqual(2);
|
|
16
|
+
|
|
17
|
+
for (const [offset, color] of gradient.values) {
|
|
18
|
+
expect(typeof offset).toBe('number');
|
|
19
|
+
expect(offset).toBeGreaterThanOrEqual(0);
|
|
20
|
+
expect(offset).toBeLessThanOrEqual(1);
|
|
21
|
+
expect(color).toMatch(/^#[0-9a-f]{6}$/i);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('are identical across light and dark mode (theme-agnostic)', () => {
|
|
27
|
+
expect(buildTheme('teal', 'dark').gradients).toBe(
|
|
28
|
+
buildTheme('teal', 'light').gradients,
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
hexToAppColorName,
|
|
6
6
|
} from '../theme/color-presets';
|
|
7
7
|
import { buildTheme } from '../theme/build-theme';
|
|
8
|
+
import { THEME_GRADIENTS } from '../theme/gradients';
|
|
8
9
|
import type { Theme, ThemeColors, ThemeMode } from '../theme/types';
|
|
9
10
|
|
|
10
11
|
describe('Theme system', () => {
|
|
@@ -111,6 +112,7 @@ describe('Theme system', () => {
|
|
|
111
112
|
isDark: false,
|
|
112
113
|
isLight: true,
|
|
113
114
|
colors: createMockColors(),
|
|
115
|
+
gradients: THEME_GRADIENTS,
|
|
114
116
|
};
|
|
115
117
|
expect(lightTheme.isDark).toBe(false);
|
|
116
118
|
expect(lightTheme.isLight).toBe(true);
|
|
@@ -120,6 +122,7 @@ describe('Theme system', () => {
|
|
|
120
122
|
isDark: true,
|
|
121
123
|
isLight: false,
|
|
122
124
|
colors: createMockColors(),
|
|
125
|
+
gradients: THEME_GRADIENTS,
|
|
123
126
|
};
|
|
124
127
|
expect(darkTheme.isDark).toBe(true);
|
|
125
128
|
expect(darkTheme.isLight).toBe(false);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react-native';
|
|
2
|
+
|
|
3
|
+
import { useGutters } from '../hooks/useGutters';
|
|
4
|
+
import { space } from '../styles/tokens';
|
|
5
|
+
|
|
6
|
+
// The RN mock reports a 375px-wide window (below the `sm` 640 breakpoint), so
|
|
7
|
+
// every case resolves to the `base` tier.
|
|
8
|
+
|
|
9
|
+
describe('useGutters', () => {
|
|
10
|
+
it('expands a single size to all four edges', () => {
|
|
11
|
+
const { result } = renderHook(() => useGutters(['base']));
|
|
12
|
+
expect(result.current).toEqual({
|
|
13
|
+
paddingTop: space.lg,
|
|
14
|
+
paddingRight: space.lg,
|
|
15
|
+
paddingBottom: space.lg,
|
|
16
|
+
paddingLeft: space.lg,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('treats a two-value list as [vertical, horizontal]', () => {
|
|
21
|
+
const { result } = renderHook(() => useGutters([0, 'wide']));
|
|
22
|
+
expect(result.current).toEqual({
|
|
23
|
+
paddingTop: 0,
|
|
24
|
+
paddingRight: space.xl,
|
|
25
|
+
paddingBottom: 0,
|
|
26
|
+
paddingLeft: space.xl,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('maps a four-value list to [top, right, bottom, left]', () => {
|
|
31
|
+
const { result } = renderHook(() =>
|
|
32
|
+
useGutters(['compact', 'base', 0, 'wide']),
|
|
33
|
+
);
|
|
34
|
+
expect(result.current).toEqual({
|
|
35
|
+
paddingTop: space.sm,
|
|
36
|
+
paddingRight: space.lg,
|
|
37
|
+
paddingBottom: 0,
|
|
38
|
+
paddingLeft: space.xl,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns zero padding for a `0` gutter', () => {
|
|
43
|
+
const { result } = renderHook(() => useGutters([0]));
|
|
44
|
+
expect(result.current).toEqual({
|
|
45
|
+
paddingTop: 0,
|
|
46
|
+
paddingRight: 0,
|
|
47
|
+
paddingBottom: 0,
|
|
48
|
+
paddingLeft: 0,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('keeps a stable object identity across re-renders at the same width', () => {
|
|
53
|
+
const { result, rerender } = renderHook(() => useGutters(['base']));
|
|
54
|
+
const first = result.current;
|
|
55
|
+
rerender({});
|
|
56
|
+
expect(result.current).toBe(first);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
import { act, renderHook } from '@testing-library/react-native';
|
|
4
|
+
import { ImpactFeedbackStyle, impactAsync } from 'expo-haptics';
|
|
5
|
+
|
|
6
|
+
import { BloomHapticsProvider, useHaptics } from '../hooks/useHaptics';
|
|
7
|
+
|
|
8
|
+
const originalOS = Platform.OS;
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
Platform.OS = originalOS;
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('useHaptics', () => {
|
|
16
|
+
it('returns a callback', () => {
|
|
17
|
+
const { result } = renderHook(() => useHaptics());
|
|
18
|
+
expect(typeof result.current).toBe('function');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('fires the mapped impact style on iOS', () => {
|
|
22
|
+
Platform.OS = 'ios';
|
|
23
|
+
const { result } = renderHook(() => useHaptics());
|
|
24
|
+
act(() => result.current('heavy'));
|
|
25
|
+
expect(impactAsync).toHaveBeenCalledWith(ImpactFeedbackStyle.Heavy);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('defaults to the light impact', () => {
|
|
29
|
+
Platform.OS = 'ios';
|
|
30
|
+
const { result } = renderHook(() => useHaptics());
|
|
31
|
+
act(() => result.current());
|
|
32
|
+
expect(impactAsync).toHaveBeenCalledWith(ImpactFeedbackStyle.Light);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('clamps to the light impact on Android regardless of strength', () => {
|
|
36
|
+
Platform.OS = 'android';
|
|
37
|
+
const { result } = renderHook(() => useHaptics());
|
|
38
|
+
act(() => result.current('heavy'));
|
|
39
|
+
expect(impactAsync).toHaveBeenCalledWith(ImpactFeedbackStyle.Light);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('no-ops on web', () => {
|
|
43
|
+
Platform.OS = 'web';
|
|
44
|
+
const { result } = renderHook(() => useHaptics());
|
|
45
|
+
act(() => result.current('medium'));
|
|
46
|
+
expect(impactAsync).not.toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('no-ops when disabled via BloomHapticsProvider', () => {
|
|
50
|
+
Platform.OS = 'ios';
|
|
51
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
52
|
+
<BloomHapticsProvider enabled={false}>{children}</BloomHapticsProvider>
|
|
53
|
+
);
|
|
54
|
+
const { result } = renderHook(() => useHaptics(), { wrapper });
|
|
55
|
+
act(() => result.current('medium'));
|
|
56
|
+
expect(impactAsync).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react-native';
|
|
2
|
+
|
|
3
|
+
import { useInteractionState } from '../hooks/useInteractionState';
|
|
4
|
+
|
|
5
|
+
describe('useInteractionState', () => {
|
|
6
|
+
it('starts inactive and toggles on onIn/onOut', () => {
|
|
7
|
+
const { result } = renderHook(() => useInteractionState());
|
|
8
|
+
expect(result.current.state).toBe(false);
|
|
9
|
+
|
|
10
|
+
act(() => result.current.onIn());
|
|
11
|
+
expect(result.current.state).toBe(true);
|
|
12
|
+
|
|
13
|
+
act(() => result.current.onOut());
|
|
14
|
+
expect(result.current.state).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('keeps handler identities stable across renders', () => {
|
|
18
|
+
const { result, rerender } = renderHook(() => useInteractionState());
|
|
19
|
+
const firstOnIn = result.current.onIn;
|
|
20
|
+
const firstOnOut = result.current.onOut;
|
|
21
|
+
|
|
22
|
+
rerender({});
|
|
23
|
+
|
|
24
|
+
expect(result.current.onIn).toBe(firstOnIn);
|
|
25
|
+
expect(result.current.onOut).toBe(firstOnOut);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { useRef } from 'react';
|
|
2
|
+
import { Pressable, Text, View } from 'react-native';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
|
+
|
|
5
|
+
import { AnimatedCheck, type AnimatedCheckRef } from './index';
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof AnimatedCheck> = {
|
|
8
|
+
title: 'Components/AnimatedCheck',
|
|
9
|
+
component: AnimatedCheck,
|
|
10
|
+
argTypes: {
|
|
11
|
+
size: { control: { type: 'range', min: 16, max: 128, step: 4 } },
|
|
12
|
+
color: { control: 'color' },
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
|
|
18
|
+
type Story = StoryObj<typeof AnimatedCheck>;
|
|
19
|
+
|
|
20
|
+
export const Playable: Story = {
|
|
21
|
+
args: { size: 64 },
|
|
22
|
+
render: (args) => {
|
|
23
|
+
function Demo() {
|
|
24
|
+
const ref = useRef<AnimatedCheckRef>(null);
|
|
25
|
+
return (
|
|
26
|
+
<View style={{ gap: 16, alignItems: 'center' }}>
|
|
27
|
+
<AnimatedCheck ref={ref} {...args} />
|
|
28
|
+
<Pressable
|
|
29
|
+
accessibilityLabel="Play"
|
|
30
|
+
onPress={() => ref.current?.play()}
|
|
31
|
+
style={{
|
|
32
|
+
paddingVertical: 8,
|
|
33
|
+
paddingHorizontal: 16,
|
|
34
|
+
borderRadius: 8,
|
|
35
|
+
backgroundColor: '#111827',
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
<Text style={{ color: 'white', fontWeight: '600' }}>Play</Text>
|
|
39
|
+
</Pressable>
|
|
40
|
+
</View>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return <Demo />;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const CustomColor: Story = {
|
|
48
|
+
args: { size: 80, color: '#6366f1' },
|
|
49
|
+
render: (args) => {
|
|
50
|
+
function Demo() {
|
|
51
|
+
const ref = useRef<AnimatedCheckRef>(null);
|
|
52
|
+
return (
|
|
53
|
+
<Pressable accessibilityLabel="Replay" onPress={() => ref.current?.play()}>
|
|
54
|
+
<AnimatedCheck ref={ref} {...args} />
|
|
55
|
+
</Pressable>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return <Demo />;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -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
|
+
});
|
|
@@ -19,6 +19,7 @@ const meta: Meta<typeof Avatar> = {
|
|
|
19
19
|
verified: { control: 'boolean' },
|
|
20
20
|
live: { control: 'boolean' },
|
|
21
21
|
hideLiveBadge: { control: 'boolean' },
|
|
22
|
+
ring: { control: 'object' },
|
|
22
23
|
},
|
|
23
24
|
};
|
|
24
25
|
|
|
@@ -29,6 +30,13 @@ type Story = StoryObj<typeof Avatar>;
|
|
|
29
30
|
const SAMPLE_URI =
|
|
30
31
|
'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200&h=200&fit=crop';
|
|
31
32
|
|
|
33
|
+
// Sample Instagram-stories-style gradient, defined locally in the story only —
|
|
34
|
+
// Bloom ships no such palette (it stays brand-neutral). Consumers supply their
|
|
35
|
+
// own ring colors via the `ring` prop.
|
|
36
|
+
const STORY_GRADIENT = ['#F58529', '#DD2A7B', '#8134AF', '#515BD4'];
|
|
37
|
+
// Muted gray used by IG for an already-seen story.
|
|
38
|
+
const SEEN_GRAY = '#C7C7CC';
|
|
39
|
+
|
|
32
40
|
export const Basic: Story = {
|
|
33
41
|
args: { size: 64, name: 'Nate Isern' },
|
|
34
42
|
};
|
|
@@ -118,3 +126,90 @@ export const LiveGallery: Story = {
|
|
|
118
126
|
),
|
|
119
127
|
name: 'Live (gallery)',
|
|
120
128
|
};
|
|
129
|
+
|
|
130
|
+
export const GradientRing: Story = {
|
|
131
|
+
args: {
|
|
132
|
+
size: 72,
|
|
133
|
+
uri: SAMPLE_URI,
|
|
134
|
+
ring: { colors: STORY_GRADIENT, width: 3, gap: 3 },
|
|
135
|
+
},
|
|
136
|
+
name: 'Gradient ring (stories)',
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const SeenVsUnseen: Story = {
|
|
140
|
+
render: () => (
|
|
141
|
+
<View style={{ flexDirection: 'row', gap: 20, alignItems: 'center' }}>
|
|
142
|
+
{/* Unseen → gradient ring. */}
|
|
143
|
+
<Avatar
|
|
144
|
+
size={72}
|
|
145
|
+
uri={SAMPLE_URI}
|
|
146
|
+
ring={{ colors: STORY_GRADIENT, width: 3, gap: 3 }}
|
|
147
|
+
/>
|
|
148
|
+
{/* Seen → muted solid gray ring. */}
|
|
149
|
+
<Avatar
|
|
150
|
+
size={72}
|
|
151
|
+
uri={SAMPLE_URI}
|
|
152
|
+
ring={{ colors: SEEN_GRAY, width: 3, gap: 3 }}
|
|
153
|
+
/>
|
|
154
|
+
</View>
|
|
155
|
+
),
|
|
156
|
+
name: 'Seen vs unseen',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const RingWithGap: Story = {
|
|
160
|
+
render: () => (
|
|
161
|
+
<View style={{ flexDirection: 'row', gap: 24, alignItems: 'center' }}>
|
|
162
|
+
{/* gap 0 → overlays the edge, footprint unchanged. */}
|
|
163
|
+
<Avatar size={72} uri={SAMPLE_URI} ring={{ colors: STORY_GRADIENT, width: 3, gap: 0 }} />
|
|
164
|
+
{/* gap 4 → sits outside the avatar, footprint grows. */}
|
|
165
|
+
<Avatar size={72} uri={SAMPLE_URI} ring={{ colors: STORY_GRADIENT, width: 3, gap: 4 }} />
|
|
166
|
+
{/* gap 8 → wider clearance. */}
|
|
167
|
+
<Avatar size={72} uri={SAMPLE_URI} ring={{ colors: STORY_GRADIENT, width: 4, gap: 8 }} />
|
|
168
|
+
</View>
|
|
169
|
+
),
|
|
170
|
+
name: 'Ring with gap',
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const RingSquircle: Story = {
|
|
174
|
+
args: {
|
|
175
|
+
size: 72,
|
|
176
|
+
uri: SAMPLE_URI,
|
|
177
|
+
shape: 'squircle',
|
|
178
|
+
ring: { colors: STORY_GRADIENT, width: 3, gap: 3 },
|
|
179
|
+
},
|
|
180
|
+
name: 'Gradient ring (squircle)',
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const SolidRing: Story = {
|
|
184
|
+
render: () => (
|
|
185
|
+
<View style={{ flexDirection: 'row', gap: 20, alignItems: 'center' }}>
|
|
186
|
+
<Avatar size={72} uri={SAMPLE_URI} ring={{ colors: '#1A73E8', width: 3 }} />
|
|
187
|
+
<Avatar size={72} uri={SAMPLE_URI} shape="squircle" ring={{ colors: '#1A73E8', width: 3 }} />
|
|
188
|
+
<Avatar size={72} uri={SAMPLE_URI} ring={{ colors: '#1A73E8', width: 3, gap: 4 }} />
|
|
189
|
+
</View>
|
|
190
|
+
),
|
|
191
|
+
name: 'Solid ring',
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const GradientDirections: Story = {
|
|
195
|
+
render: () => (
|
|
196
|
+
<View style={{ flexDirection: 'row', gap: 20, alignItems: 'center' }}>
|
|
197
|
+
<Avatar
|
|
198
|
+
size={72}
|
|
199
|
+
uri={SAMPLE_URI}
|
|
200
|
+
ring={{ colors: STORY_GRADIENT, width: 3, gap: 3, gradientDirection: 'diagonal' }}
|
|
201
|
+
/>
|
|
202
|
+
<Avatar
|
|
203
|
+
size={72}
|
|
204
|
+
uri={SAMPLE_URI}
|
|
205
|
+
ring={{ colors: STORY_GRADIENT, width: 3, gap: 3, gradientDirection: 'horizontal' }}
|
|
206
|
+
/>
|
|
207
|
+
<Avatar
|
|
208
|
+
size={72}
|
|
209
|
+
uri={SAMPLE_URI}
|
|
210
|
+
ring={{ colors: STORY_GRADIENT, width: 3, gap: 3, gradientDirection: 'vertical' }}
|
|
211
|
+
/>
|
|
212
|
+
</View>
|
|
213
|
+
),
|
|
214
|
+
name: 'Gradient directions',
|
|
215
|
+
};
|