feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,439 @@
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
import * as React from 'react';
import { Animated, Easing, I18nManager, Platform, ScrollView, StyleSheet, View } from 'react-native';
import color from 'color';
import { getCombinedStyles, getFABColors, getLabelSizeWeb } from './utils';
import { useInternalTheme } from '../../core/theming';
import Icon from '../Icon';
import Surface from '../Surface';
import TouchableRipple from '../TouchableRipple/TouchableRipple';
import AnimatedText from '../Typography/AnimatedText';
const SIZE = 56;
const SCALE = 0.9;
const SHADOW_LAYER_Z_INDEX = 1;
const CONTENT_LAYER_Z_INDEX = 2;
/**
* An animated, extending horizontally floating action button represents the primary action in an application.
*
* ## Usage
* ```js
* import React from 'react';
* import {
* StyleProp,
* ViewStyle,
* Animated,
* StyleSheet,
* Platform,
* ScrollView,
* Text,
* SafeAreaView,
* I18nManager,
* } from 'react-native';
* import { AnimatedFAB } from 'react-native-paper';
*
* const MyComponent = ({
* animatedValue,
* visible,
* extended,
* label,
* animateFrom,
* style,
* iconMode,
* }) => {
* const [isExtended, setIsExtended] = React.useState(true);
*
* const isIOS = Platform.OS === 'ios';
*
* const onScroll = ({ nativeEvent }) => {
* const currentScrollPosition =
* Math.floor(nativeEvent?.contentOffset?.y) ?? 0;
*
* setIsExtended(currentScrollPosition <= 0);
* };
*
* const fabStyle = { [animateFrom]: 16 };
*
* return (
* <SafeAreaView style={styles.container}>
* <ScrollView onScroll={onScroll}>
* {[...new Array(100).keys()].map((_, i) => (
* <Text>{i}</Text>
* ))}
* </ScrollView>
* <AnimatedFAB
* icon={'plus'}
* label={'Label'}
* extended={isExtended}
* onPress={() => console.log('Pressed')}
* visible={visible}
* animateFrom={'right'}
* iconMode={'static'}
* style={[styles.fabStyle, style, fabStyle]}
* />
* </SafeAreaView>
* );
* };
*
* export default MyComponent;
*
* const styles = StyleSheet.create({
* container: {
* flexGrow: 1,
* },
* fabStyle: {
* bottom: 16,
* right: 16,
* position: 'absolute',
* },
* });
* ```
*/
const AnimatedFAB = ({
icon,
label,
background,
accessibilityLabel = label,
accessibilityState,
color: customColor,
rippleColor: customRippleColor,
disabled,
onPress,
onLongPress,
delayLongPress,
theme: themeOverrides,
style,
visible = true,
uppercase: uppercaseProp,
testID = 'animated-fab',
animateFrom = 'right',
extended = false,
iconMode = 'dynamic',
variant = 'primary',
labelMaxFontSizeMultiplier,
hitSlop,
...rest
}) => {
const theme = useInternalTheme(themeOverrides);
const uppercase = uppercaseProp ?? !theme.isV3;
const isIOS = Platform.OS === 'ios';
const isWeb = Platform.OS === 'web';
const isAnimatedFromRight = animateFrom === 'right';
const isIconStatic = iconMode === 'static';
const {
isRTL
} = I18nManager;
const labelRef = React.useRef(null);
const {
current: visibility
} = React.useRef(new Animated.Value(visible ? 1 : 0));
const {
current: animFAB
} = React.useRef(new Animated.Value(0));
const {
isV3,
animation
} = theme;
const {
scale
} = animation;
const labelSize = isWeb ? getLabelSizeWeb(labelRef) : null;
const [textWidth, setTextWidth] = React.useState((labelSize === null || labelSize === void 0 ? void 0 : labelSize.width) ?? 0);
const [textHeight, setTextHeight] = React.useState((labelSize === null || labelSize === void 0 ? void 0 : labelSize.height) ?? 0);
const borderRadius = SIZE / (isV3 ? 3.5 : 2);
React.useEffect(() => {
if (!isWeb) {
return;
}
const updateTextSize = () => {
if (labelRef.current) {
const labelSize = getLabelSizeWeb(labelRef);
if (labelSize) {
setTextHeight(labelSize.height ?? 0);
setTextWidth(labelSize.width ?? 0);
}
}
};
updateTextSize();
window.addEventListener('resize', updateTextSize);
return () => {
if (!isWeb) {
return;
}
window.removeEventListener('resize', updateTextSize);
};
}, [isWeb]);
React.useEffect(() => {
if (visible) {
Animated.timing(visibility, {
toValue: 1,
duration: 200 * scale,
useNativeDriver: true
}).start();
} else {
Animated.timing(visibility, {
toValue: 0,
duration: 150 * scale,
useNativeDriver: true
}).start();
}
}, [visible, scale, visibility]);
const {
backgroundColor: customBackgroundColor,
...restStyle
} = StyleSheet.flatten(style) || {};
const {
backgroundColor,
foregroundColor
} = getFABColors({
theme,
variant,
disabled,
customColor,
customBackgroundColor
});
const rippleColor = customRippleColor || color(foregroundColor).alpha(0.12).rgb().string();
const extendedWidth = textWidth + SIZE + borderRadius;
const distance = isAnimatedFromRight ? -textWidth - borderRadius : textWidth + borderRadius;
React.useEffect(() => {
Animated.timing(animFAB, {
toValue: !extended ? 0 : distance,
duration: 150 * scale,
useNativeDriver: true,
easing: Easing.linear
}).start();
}, [animFAB, scale, distance, extended]);
const onTextLayout = ({
nativeEvent
}) => {
var _nativeEvent$lines$, _nativeEvent$lines$2;
const currentWidth = Math.ceil(((_nativeEvent$lines$ = nativeEvent.lines[0]) === null || _nativeEvent$lines$ === void 0 ? void 0 : _nativeEvent$lines$.width) ?? 0);
const currentHeight = Math.ceil(((_nativeEvent$lines$2 = nativeEvent.lines[0]) === null || _nativeEvent$lines$2 === void 0 ? void 0 : _nativeEvent$lines$2.height) ?? 0);
if (currentWidth !== textWidth || currentHeight !== textHeight) {
setTextHeight(currentHeight);
if (isIOS) {
return setTextWidth(currentWidth - 12);
}
setTextWidth(currentWidth);
}
};
const propForDirection = right => {
if (isAnimatedFromRight) {
return right;
}
return right.reverse();
};
const combinedStyles = getCombinedStyles({
isAnimatedFromRight,
isIconStatic,
distance,
animFAB
});
const font = isV3 ? theme.fonts.labelLarge : theme.fonts.medium;
const textStyle = {
color: foregroundColor,
...font
};
const md2Elevation = disabled || !isIOS ? 0 : 6;
const md3Elevation = disabled || !isIOS ? 0 : 3;
const shadowStyle = isV3 ? styles.v3Shadow : styles.shadow;
const baseStyle = [StyleSheet.absoluteFill, disabled ? styles.disabled : shadowStyle];
const newAccessibilityState = {
...accessibilityState,
disabled
};
return /*#__PURE__*/React.createElement(Surface, _extends({}, rest, {
testID: `${testID}-container`,
style: [{
opacity: visibility,
transform: [{
scale: visibility
}],
borderRadius
}, !isV3 && {
elevation: md2Elevation
}, styles.container, restStyle]
}, isV3 && {
elevation: md3Elevation
}, {
theme: theme,
container: true
}), /*#__PURE__*/React.createElement(Animated.View, {
style: [!isV3 && {
transform: [{
scaleY: animFAB.interpolate({
inputRange: propForDirection([distance, 0]),
outputRange: propForDirection([SCALE, 1])
})
}]
}, styles.standard, {
borderRadius
}]
}, /*#__PURE__*/React.createElement(View, {
style: [StyleSheet.absoluteFill, styles.shadowWrapper]
}, /*#__PURE__*/React.createElement(Animated.View, {
pointerEvents: "none",
style: [baseStyle, {
width: extendedWidth,
opacity: animFAB.interpolate({
inputRange: propForDirection([distance, 0.9 * distance, 0]),
outputRange: propForDirection([1, 0.15, 0])
}),
borderRadius
}],
testID: `${testID}-extended-shadow`
}), /*#__PURE__*/React.createElement(Animated.View, {
pointerEvents: "none",
style: [baseStyle, {
opacity: animFAB.interpolate({
inputRange: propForDirection([distance, 0.9 * distance, 0]),
outputRange: propForDirection([0, 0.85, 1])
}),
width: SIZE,
borderRadius: animFAB.interpolate({
inputRange: propForDirection([distance, 0]),
outputRange: propForDirection([SIZE / (extendedWidth / SIZE), borderRadius])
})
}, combinedStyles.absoluteFill],
testID: `${testID}-shadow`
})), /*#__PURE__*/React.createElement(Animated.View, {
pointerEvents: "box-none",
style: [styles.innerWrapper, {
borderRadius
}]
}, /*#__PURE__*/React.createElement(Animated.View, {
style: [styles.standard, {
width: extendedWidth,
backgroundColor,
borderRadius
}, combinedStyles.innerWrapper]
}, /*#__PURE__*/React.createElement(TouchableRipple, {
borderless: true,
background: background,
onPress: onPress,
onLongPress: onLongPress,
delayLongPress: delayLongPress,
rippleColor: rippleColor,
disabled: disabled,
accessibilityLabel: accessibilityLabel,
accessibilityRole: "button",
accessibilityState: newAccessibilityState,
testID: testID,
style: {
borderRadius
},
theme: theme,
hitSlop: hitSlop
}, /*#__PURE__*/React.createElement(View, {
style: [styles.standard, {
width: extendedWidth,
borderRadius
}]
}))))), /*#__PURE__*/React.createElement(Animated.View, {
style: [styles.iconWrapper, combinedStyles.iconWrapper],
pointerEvents: "none"
}, /*#__PURE__*/React.createElement(Icon, {
source: icon,
size: 24,
color: foregroundColor,
theme: theme
})), /*#__PURE__*/React.createElement(View, {
pointerEvents: "none"
}, /*#__PURE__*/React.createElement(AnimatedText, {
ref: isWeb ? labelRef : null,
variant: "labelLarge",
numberOfLines: 1,
onTextLayout: isIOS ? onTextLayout : undefined,
ellipsizeMode: 'tail',
style: [{
[isAnimatedFromRight || isRTL ? 'right' : 'left']: isIconStatic ? textWidth - SIZE + borderRadius / (isV3 ? 1 : 2) : borderRadius
}, {
minWidth: textWidth,
top: -SIZE / 2 - textHeight / 2,
opacity: animFAB.interpolate({
inputRange: propForDirection([distance, 0.7 * distance, 0]),
outputRange: propForDirection([1, 0, 0])
}),
// TODO: check
transform: [{
translateX: animFAB.interpolate({
inputRange: propForDirection([distance, 0]),
outputRange: propForDirection([0, SIZE])
})
}]
}, styles.label, uppercase && styles.uppercaseLabel, textStyle],
theme: theme,
testID: `${testID}-text`,
maxFontSizeMultiplier: labelMaxFontSizeMultiplier
}, label)), !isIOS &&
/*#__PURE__*/
// Method `onTextLayout` on Android returns sizes of text visible on the screen,
// however during render the text in `FAB` isn't fully visible. In order to get
// proper text measurements there is a need to additionaly render that text, but
// wrapped in absolutely positioned `ScrollView` which height is 0.
React.createElement(ScrollView, {
style: styles.textPlaceholderContainer
}, /*#__PURE__*/React.createElement(AnimatedText, {
variant: "labelLarge",
numberOfLines: 1,
onTextLayout: onTextLayout,
ellipsizeMode: 'tail',
style: [styles.label, uppercase && styles.uppercaseLabel, textStyle],
theme: theme
}, label)));
};
const styles = StyleSheet.create({
standard: {
height: SIZE
},
disabled: {
elevation: 0
},
// eslint-disable-next-line react-native/no-color-literals
container: {
position: 'absolute',
backgroundColor: 'transparent'
},
innerWrapper: {
flexDirection: 'row',
overflow: 'hidden',
...Platform.select({
android: {
zIndex: CONTENT_LAYER_Z_INDEX
}
})
},
shadowWrapper: {
elevation: 0,
...Platform.select({
android: {
zIndex: SHADOW_LAYER_Z_INDEX
}
})
},
shadow: {
elevation: 6
},
v3Shadow: {
elevation: 3
},
iconWrapper: {
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
height: SIZE,
width: SIZE
},
label: {
position: 'absolute'
},
uppercaseLabel: {
textTransform: 'uppercase'
},
textPlaceholderContainer: {
height: 0,
position: 'absolute'
}
});
export default AnimatedFAB;
//# sourceMappingURL=AnimatedFAB.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,209 @@
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
import * as React from 'react';
import { Animated, StyleSheet, View } from 'react-native';
import { getExtendedFabStyle, getFABColors, getFabStyle } from './utils';
import { useInternalTheme } from '../../core/theming';
import { forwardRef } from '../../utils/forwardRef';
import ActivityIndicator from '../ActivityIndicator';
import CrossFadeIcon from '../CrossFadeIcon';
import Icon from '../Icon';
import Surface from '../Surface';
import TouchableRipple from '../TouchableRipple/TouchableRipple';
import Text from '../Typography/Text';
/**
* A floating action button represents the primary action on a screen. It appears in front of all screen content.
*
* ## Usage
* ```js
* import * as React from 'react';
* import { StyleSheet } from 'react-native';
* import { FAB } from 'react-native-paper';
*
* const MyComponent = () => (
* <FAB
* icon="plus"
* style={styles.fab}
* onPress={() => console.log('Pressed')}
* />
* );
*
* const styles = StyleSheet.create({
* fab: {
* position: 'absolute',
* margin: 16,
* right: 0,
* bottom: 0,
* },
* })
*
* export default MyComponent;
* ```
*/
const FAB = forwardRef(({
icon,
label,
background,
accessibilityLabel = label,
accessibilityState,
animated = true,
color: customColor,
rippleColor: customRippleColor,
disabled,
onPress,
onLongPress,
delayLongPress,
theme: themeOverrides,
style,
visible = true,
uppercase: uppercaseProp,
loading,
testID = 'fab',
size = 'medium',
customSize,
mode = 'elevated',
variant = 'primary',
labelMaxFontSizeMultiplier,
...rest
}, ref) => {
const theme = useInternalTheme(themeOverrides);
const uppercase = uppercaseProp ?? !theme.isV3;
const {
current: visibility
} = React.useRef(new Animated.Value(visible ? 1 : 0));
const {
isV3,
animation
} = theme;
const {
scale
} = animation;
React.useEffect(() => {
if (visible) {
Animated.timing(visibility, {
toValue: 1,
duration: 200 * scale,
useNativeDriver: true
}).start();
} else {
Animated.timing(visibility, {
toValue: 0,
duration: 150 * scale,
useNativeDriver: true
}).start();
}
}, [visible, scale, visibility]);
const IconComponent = animated ? CrossFadeIcon : Icon;
const fabStyle = getFabStyle({
customSize,
size,
theme
});
const {
borderRadius = fabStyle.borderRadius,
backgroundColor: customBackgroundColor
} = StyleSheet.flatten(style) || {};
const {
backgroundColor,
foregroundColor,
rippleColor
} = getFABColors({
theme,
variant,
disabled,
customColor,
customBackgroundColor,
customRippleColor
});
const isLargeSize = size === 'large';
const isFlatMode = mode === 'flat';
const iconSize = isLargeSize ? 36 : 24;
const loadingIndicatorSize = isLargeSize ? 24 : 18;
const font = isV3 ? theme.fonts.labelLarge : theme.fonts.medium;
const extendedStyle = getExtendedFabStyle({
customSize,
theme
});
const textStyle = {
color: foregroundColor,
...font
};
const md3Elevation = isFlatMode || disabled ? 0 : 3;
const newAccessibilityState = {
...accessibilityState,
disabled
};
return /*#__PURE__*/React.createElement(Surface, _extends({
ref: ref
}, rest, {
style: [{
borderRadius,
backgroundColor,
opacity: visibility,
transform: [{
scale: visibility
}]
}, !isV3 && styles.elevated, !isV3 && disabled && styles.disabled, style],
pointerEvents: visible ? 'auto' : 'none',
testID: `${testID}-container`
}, isV3 && {
elevation: md3Elevation
}, {
container: true
}), /*#__PURE__*/React.createElement(TouchableRipple, _extends({
borderless: true,
background: background,
onPress: onPress,
onLongPress: onLongPress,
delayLongPress: delayLongPress,
rippleColor: rippleColor,
disabled: disabled,
accessibilityLabel: accessibilityLabel,
accessibilityRole: "button",
accessibilityState: newAccessibilityState,
testID: testID,
style: {
borderRadius
}
}, rest), /*#__PURE__*/React.createElement(View, {
style: [styles.content, label ? extendedStyle : fabStyle],
testID: `${testID}-content`,
pointerEvents: "none"
}, icon && loading !== true ? /*#__PURE__*/React.createElement(IconComponent, {
source: icon,
size: customSize ? customSize / 2 : iconSize,
color: foregroundColor
}) : null, loading ? /*#__PURE__*/React.createElement(ActivityIndicator, {
size: customSize ? customSize / 2 : loadingIndicatorSize,
color: foregroundColor
}) : null, label ? /*#__PURE__*/React.createElement(Text, {
variant: "labelLarge",
selectable: false,
testID: `${testID}-text`,
style: [styles.label, uppercase && styles.uppercaseLabel, textStyle],
maxFontSizeMultiplier: labelMaxFontSizeMultiplier
}, label) : null)));
});
const styles = StyleSheet.create({
elevated: {
elevation: 6
},
content: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
},
label: {
marginHorizontal: 8
},
uppercaseLabel: {
textTransform: 'uppercase'
},
disabled: {
elevation: 0
}
});
export default FAB;
// @component-docs ignore-next-line
export { FAB };
//# sourceMappingURL=FAB.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,340 @@
import * as React from 'react';
import { Animated, Pressable, StyleSheet, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import FAB from './FAB';
import { getFABGroupColors } from './utils';
import { useInternalTheme } from '../../core/theming';
import Card from '../Card/Card';
import Text from '../Typography/Text';
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
/**
* A component to display a stack of FABs with related actions in a speed dial.
* To render the group above other components, you'll need to wrap it with the [`Portal`](../Portal) component.
*
* ## Usage
* ```js
* import * as React from 'react';
* import { FAB, Portal, PaperProvider } from 'react-native-paper';
*
* const MyComponent = () => {
* const [state, setState] = React.useState({ open: false });
*
* const onStateChange = ({ open }) => setState({ open });
*
* const { open } = state;
*
* return (
* <PaperProvider>
* <Portal>
* <FAB.Group
* open={open}
* visible
* icon={open ? 'calendar-today' : 'plus'}
* actions={[
* { icon: 'plus', onPress: () => console.log('Pressed add') },
* {
* icon: 'star',
* label: 'Star',
* onPress: () => console.log('Pressed star'),
* },
* {
* icon: 'email',
* label: 'Email',
* onPress: () => console.log('Pressed email'),
* },
* {
* icon: 'bell',
* label: 'Remind',
* onPress: () => console.log('Pressed notifications'),
* },
* ]}
* onStateChange={onStateChange}
* onPress={() => {
* if (open) {
* // do something if the speed dial is open
* }
* }}
* />
* </Portal>
* </PaperProvider>
* );
* };
*
* export default MyComponent;
* ```
*/
const FABGroup = ({
actions,
icon,
open,
onPress,
onLongPress,
toggleStackOnLongPress,
accessibilityLabel,
theme: themeOverrides,
style,
fabStyle,
visible,
label,
testID,
onStateChange,
color: colorProp,
delayLongPress = 200,
variant = 'primary',
enableLongPressWhenStackOpened = false,
backdropColor: customBackdropColor,
rippleColor
}) => {
const theme = useInternalTheme(themeOverrides);
const {
top,
bottom,
right,
left
} = useSafeAreaInsets();
const {
current: backdrop
} = React.useRef(new Animated.Value(0));
const animations = React.useRef(actions.map(() => new Animated.Value(open ? 1 : 0)));
const [isClosingAnimationFinished, setIsClosingAnimationFinished] = React.useState(false);
const [prevActions, setPrevActions] = React.useState(null);
const {
scale
} = theme.animation;
const {
isV3
} = theme;
React.useEffect(() => {
if (open) {
setIsClosingAnimationFinished(false);
Animated.parallel([Animated.timing(backdrop, {
toValue: 1,
duration: 250 * scale,
useNativeDriver: true
}), Animated.stagger(isV3 ? 15 : 50 * scale, animations.current.map(animation => Animated.timing(animation, {
toValue: 1,
duration: 150 * scale,
useNativeDriver: true
})).reverse())]).start();
} else {
Animated.parallel([Animated.timing(backdrop, {
toValue: 0,
duration: 200 * scale,
useNativeDriver: true
}), ...animations.current.map(animation => Animated.timing(animation, {
toValue: 0,
duration: 150 * scale,
useNativeDriver: true
}))]).start(({
finished
}) => {
if (finished) {
setIsClosingAnimationFinished(true);
}
});
}
}, [open, actions, backdrop, scale, isV3]);
const close = () => onStateChange({
open: false
});
const toggle = () => onStateChange({
open: !open
});
const handlePress = e => {
onPress === null || onPress === void 0 || onPress(e);
if (!toggleStackOnLongPress || open) {
toggle();
}
};
const handleLongPress = e => {
if (!open || enableLongPressWhenStackOpened) {
onLongPress === null || onLongPress === void 0 || onLongPress(e);
if (toggleStackOnLongPress) {
toggle();
}
}
};
const {
labelColor,
backdropColor,
stackedFABBackgroundColor
} = getFABGroupColors({
theme,
customBackdropColor
});
const backdropOpacity = open ? backdrop.interpolate({
inputRange: [0, 0.5, 1],
outputRange: [0, 1, 1]
}) : backdrop;
const opacities = animations.current;
const scales = opacities.map(opacity => open ? opacity.interpolate({
inputRange: [0, 1],
outputRange: [0.5, 1]
}) : 1);
const translations = opacities.map(opacity => open ? opacity.interpolate({
inputRange: [0, 1],
outputRange: [24, -8]
}) : -8);
const labelTranslations = opacities.map(opacity => open ? opacity.interpolate({
inputRange: [0, 1],
outputRange: [8, -8]
}) : -8);
const containerPaddings = {
paddingBottom: bottom,
paddingRight: right,
paddingLeft: left,
paddingTop: top
};
const actionsContainerVisibility = {
display: isClosingAnimationFinished ? 'none' : 'flex'
};
if (actions.length !== (prevActions === null || prevActions === void 0 ? void 0 : prevActions.length)) {
animations.current = actions.map((_, i) => animations.current[i] || new Animated.Value(open ? 1 : 0));
setPrevActions(actions);
}
return /*#__PURE__*/React.createElement(View, {
pointerEvents: "box-none",
style: [styles.container, containerPaddings, style]
}, /*#__PURE__*/React.createElement(AnimatedPressable, {
accessibilityRole: "button",
onPress: close,
pointerEvents: open ? 'auto' : 'none',
style: [styles.backdrop, {
opacity: backdropOpacity,
backgroundColor: backdropColor
}]
}), /*#__PURE__*/React.createElement(View, {
pointerEvents: "box-none",
style: styles.safeArea
}, /*#__PURE__*/React.createElement(View, {
pointerEvents: open ? 'box-none' : 'none',
style: actionsContainerVisibility
}, actions.map((it, i) => {
const labelTextStyle = {
color: it.labelTextColor ?? labelColor,
...(isV3 ? theme.fonts.titleMedium : {})
};
const marginHorizontal = typeof it.size === 'undefined' || it.size === 'small' ? 24 : 16;
const accessibilityLabel = typeof it.accessibilityLabel !== 'undefined' ? it.accessibilityLabel : it.label;
const size = typeof it.size !== 'undefined' ? it.size : 'small';
const handleActionPress = e => {
it.onPress(e);
close();
};
return /*#__PURE__*/React.createElement(View, {
key: i // eslint-disable-line react/no-array-index-key
,
style: [styles.item, {
marginHorizontal
}, it.wrapperStyle],
pointerEvents: open ? 'box-none' : 'none',
accessibilityRole: "button",
importantForAccessibility: open ? 'yes' : 'no-hide-descendants',
accessibilityElementsHidden: !open,
accessible: open,
accessibilityLabel: accessibilityLabel
}, it.label && /*#__PURE__*/React.createElement(View, null, /*#__PURE__*/React.createElement(Card, {
mode: isV3 ? 'contained' : 'elevated',
onPress: handleActionPress,
accessibilityHint: it.accessibilityHint,
importantForAccessibility: "no-hide-descendants",
accessibilityElementsHidden: true,
style: [styles.containerStyle, {
transform: [isV3 ? {
translateY: labelTranslations[i]
} : {
scale: scales[i]
}],
opacity: opacities[i]
}, isV3 && styles.v3ContainerStyle, it.containerStyle]
}, /*#__PURE__*/React.createElement(Text, {
variant: "titleMedium",
importantForAccessibility: "no-hide-descendants",
accessibilityElementsHidden: true,
style: [labelTextStyle, it.labelStyle],
maxFontSizeMultiplier: it.labelMaxFontSizeMultiplier
}, it.label))), /*#__PURE__*/React.createElement(FAB, {
size: size,
icon: it.icon,
color: it.color,
style: [{
transform: [{
scale: scales[i]
}],
opacity: opacities[i],
backgroundColor: stackedFABBackgroundColor
}, isV3 && {
transform: [{
translateY: translations[i]
}]
}, it.style],
accessibilityElementsHidden: true,
theme: theme,
onPress: handleActionPress,
importantForAccessibility: "no-hide-descendants",
testID: it.testID,
visible: open,
rippleColor: it.rippleColor
}));
})), /*#__PURE__*/React.createElement(FAB, {
onPress: handlePress,
onLongPress: handleLongPress,
delayLongPress: delayLongPress,
icon: icon,
color: colorProp,
accessibilityLabel: accessibilityLabel,
accessibilityRole: "button",
accessibilityState: {
expanded: open
},
style: [styles.fab, fabStyle],
theme: theme,
visible: visible,
label: label,
testID: testID,
variant: variant,
rippleColor: rippleColor
})));
};
FABGroup.displayName = 'FAB.Group';
export default FABGroup;
// @component-docs ignore-next-line
export { FABGroup };
const styles = StyleSheet.create({
safeArea: {
alignItems: 'flex-end'
},
container: {
...StyleSheet.absoluteFill,
justifyContent: 'flex-end'
},
fab: {
marginHorizontal: 16,
marginBottom: 16,
marginTop: 0
},
backdrop: {
...StyleSheet.absoluteFill
},
containerStyle: {
borderRadius: 5,
paddingHorizontal: 12,
paddingVertical: 6,
marginVertical: 8,
marginHorizontal: 16,
elevation: 2
},
item: {
marginBottom: 16,
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center'
},
// eslint-disable-next-line react-native/no-color-literals
v3ContainerStyle: {
backgroundColor: 'transparent',
elevation: 0
}
});
//# sourceMappingURL=FABGroup.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
import FABComponent from './FAB';
import FABGroup from './FABGroup';
const FAB = Object.assign(
// @component ./FAB.tsx
FABComponent, {
// @component ./FABGroup.tsx
Group: FABGroup
});
export default FAB;
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["FABComponent","FABGroup","FAB","Object","assign","Group"],"sourceRoot":"../../../../src","sources":["components/FAB/index.ts"],"mappings":"AAAA,OAAOA,YAAY,MAAM,OAAO;AAChC,OAAOC,QAAQ,MAAM,YAAY;AAEjC,MAAMC,GAAG,GAAGC,MAAM,CAACC,MAAM;AACvB;AACAJ,YAAY,EACZ;EACE;EACAK,KAAK,EAAEJ;AACT,CACF,CAAC;AAED,eAAeC,GAAG","ignoreList":[]}

View File

@@ -0,0 +1,368 @@
import { I18nManager, Platform } from 'react-native';
import color from 'color';
import { black, white } from '../../styles/themes/v2/colors';
import getContrastingColor from '../../utils/getContrastingColor';
export const getCombinedStyles = ({
isAnimatedFromRight,
isIconStatic,
distance,
animFAB
}) => {
const {
isRTL
} = I18nManager;
const defaultPositionStyles = {
left: -distance,
right: undefined
};
const combinedStyles = {
innerWrapper: {
...defaultPositionStyles
},
iconWrapper: {
...defaultPositionStyles
},
absoluteFill: {}
};
const animatedFromRight = isAnimatedFromRight && !isRTL;
const animatedFromRightRTL = isAnimatedFromRight && isRTL;
const animatedFromLeft = !isAnimatedFromRight && !isRTL;
const animatedFromLeftRTL = !isAnimatedFromRight && isRTL;
if (animatedFromRight) {
combinedStyles.innerWrapper.transform = [{
translateX: animFAB.interpolate({
inputRange: [distance, 0],
outputRange: [distance, 0]
})
}];
combinedStyles.iconWrapper.transform = [{
translateX: isIconStatic ? 0 : animFAB
}];
combinedStyles.absoluteFill.transform = [{
translateX: animFAB.interpolate({
inputRange: [distance, 0],
outputRange: [Math.abs(distance) / 2, Math.abs(distance)]
})
}];
} else if (animatedFromRightRTL) {
combinedStyles.iconWrapper.transform = [{
translateX: isIconStatic ? 0 : animFAB.interpolate({
inputRange: [distance, 0],
outputRange: [-distance, 0]
})
}];
combinedStyles.innerWrapper.transform = [{
translateX: animFAB.interpolate({
inputRange: [distance, 0],
outputRange: [-distance, 0]
})
}];
combinedStyles.absoluteFill.transform = [{
translateX: animFAB.interpolate({
inputRange: [distance, 0],
outputRange: [0, distance]
})
}];
} else if (animatedFromLeft) {
combinedStyles.iconWrapper.transform = [{
translateX: isIconStatic ? distance : animFAB.interpolate({
inputRange: [0, distance],
outputRange: [distance, distance * 2]
})
}];
combinedStyles.innerWrapper.transform = [{
translateX: animFAB
}];
combinedStyles.absoluteFill.transform = [{
translateX: animFAB.interpolate({
inputRange: [0, distance],
outputRange: [0, Math.abs(distance) / 2]
})
}];
} else if (animatedFromLeftRTL) {
combinedStyles.iconWrapper.transform = [{
translateX: isIconStatic ? animFAB.interpolate({
inputRange: [0, distance],
outputRange: [-distance, -distance * 2]
}) : -distance
}];
combinedStyles.innerWrapper.transform = [{
translateX: animFAB.interpolate({
inputRange: [0, distance],
outputRange: [0, -distance]
})
}];
combinedStyles.absoluteFill.transform = [{
translateX: animFAB.interpolate({
inputRange: [0, distance],
outputRange: [0, -distance]
})
}];
}
return combinedStyles;
};
const getBackgroundColor = ({
theme,
isVariant,
disabled,
customBackgroundColor
}) => {
var _theme$colors;
if (customBackgroundColor && !disabled) {
return customBackgroundColor;
}
if (theme.isV3) {
if (disabled) {
return theme.colors.surfaceDisabled;
}
if (isVariant('primary')) {
return theme.colors.primaryContainer;
}
if (isVariant('secondary')) {
return theme.colors.secondaryContainer;
}
if (isVariant('tertiary')) {
return theme.colors.tertiaryContainer;
}
if (isVariant('surface')) {
return theme.colors.elevation.level3;
}
}
if (disabled) {
if (theme.dark) {
return color(white).alpha(0.12).rgb().string();
}
return color(black).alpha(0.12).rgb().string();
}
//@ts-ignore
return (_theme$colors = theme.colors) === null || _theme$colors === void 0 ? void 0 : _theme$colors.accent;
};
const getForegroundColor = ({
theme,
isVariant,
disabled,
backgroundColor,
customColor
}) => {
if (typeof customColor !== 'undefined' && !disabled) {
return customColor;
}
if (theme.isV3) {
if (disabled) {
return theme.colors.onSurfaceDisabled;
}
if (isVariant('primary')) {
return theme.colors.onPrimaryContainer;
}
if (isVariant('secondary')) {
return theme.colors.onSecondaryContainer;
}
if (isVariant('tertiary')) {
return theme.colors.onTertiaryContainer;
}
if (isVariant('surface')) {
return theme.colors.primary;
}
}
if (disabled) {
if (theme.dark) {
return color(white).alpha(0.32).rgb().string();
}
return color(black).alpha(0.32).rgb().string();
}
if (backgroundColor) {
return getContrastingColor(backgroundColor || white, white, 'rgba(0, 0, 0, .54)');
}
return getContrastingColor(white, white, 'rgba(0, 0, 0, .54)');
};
export const getFABColors = ({
theme,
variant,
disabled,
customColor,
customBackgroundColor,
customRippleColor
}) => {
const isVariant = variantToCompare => {
return variant === variantToCompare;
};
const baseFABColorProps = {
theme,
isVariant,
disabled
};
const backgroundColor = getBackgroundColor({
...baseFABColorProps,
customBackgroundColor
});
const foregroundColor = getForegroundColor({
...baseFABColorProps,
customColor,
backgroundColor
});
return {
backgroundColor,
foregroundColor,
rippleColor: customRippleColor || color(foregroundColor).alpha(0.12).rgb().string()
};
};
const getLabelColor = ({
theme
}) => {
if (theme.isV3) {
return theme.colors.onSurface;
}
if (theme.dark) {
return theme.colors.text;
}
return color(theme.colors.text).fade(0.54).rgb().string();
};
const getBackdropColor = ({
theme,
customBackdropColor
}) => {
var _theme$colors2;
if (customBackdropColor) {
return customBackdropColor;
}
if (theme.isV3) {
return color(theme.colors.background).alpha(0.95).rgb().string();
}
return (_theme$colors2 = theme.colors) === null || _theme$colors2 === void 0 ? void 0 : _theme$colors2.backdrop;
};
const getStackedFABBackgroundColor = ({
theme
}) => {
if (theme.isV3) {
return theme.colors.elevation.level3;
}
return theme.colors.surface;
};
export const getFABGroupColors = ({
theme,
customBackdropColor
}) => {
return {
labelColor: getLabelColor({
theme
}),
backdropColor: getBackdropColor({
theme,
customBackdropColor
}),
stackedFABBackgroundColor: getStackedFABBackgroundColor({
theme
})
};
};
const standardSize = {
height: 56,
width: 56,
borderRadius: 28
};
const smallSize = {
height: 40,
width: 40,
borderRadius: 28
};
const v3SmallSize = {
height: 40,
width: 40
};
const v3MediumSize = {
height: 56,
width: 56
};
const v3LargeSize = {
height: 96,
width: 96
};
const getCustomFabSize = (customSize, roundness) => ({
height: customSize,
width: customSize,
borderRadius: roundness === 0 ? 0 : customSize / roundness
});
export const getFabStyle = ({
size,
theme,
customSize
}) => {
const {
isV3,
roundness
} = theme;
if (customSize) return getCustomFabSize(customSize, roundness);
if (isV3) {
switch (size) {
case 'small':
return {
...v3SmallSize,
borderRadius: 3 * roundness
};
case 'medium':
return {
...v3MediumSize,
borderRadius: 4 * roundness
};
case 'large':
return {
...v3LargeSize,
borderRadius: 7 * roundness
};
}
}
if (size === 'small') {
return smallSize;
}
return standardSize;
};
const extended = {
height: 48,
paddingHorizontal: 16
};
const v3Extended = {
height: 56,
borderRadius: 16,
paddingHorizontal: 16
};
const getExtendedFabDimensions = customSize => ({
height: customSize,
paddingHorizontal: 16
});
export const getExtendedFabStyle = ({
customSize,
theme
}) => {
if (customSize) return getExtendedFabDimensions(customSize);
const {
isV3
} = theme;
return isV3 ? v3Extended : extended;
};
let cachedContext = null;
const getCanvasContext = () => {
if (cachedContext) {
return cachedContext;
}
const canvas = document.createElement('canvas');
cachedContext = canvas.getContext('2d');
return cachedContext;
};
export const getLabelSizeWeb = ref => {
if (Platform.OS !== 'web' || ref.current === null) {
return null;
}
const canvasContext = getCanvasContext();
if (!canvasContext) {
return null;
}
const elementStyles = window.getComputedStyle(ref.current);
canvasContext.font = elementStyles.font;
const metrics = canvasContext.measureText(ref.current.innerText);
return {
width: metrics.width,
height: (metrics.fontBoundingBoxAscent ?? 0) + (metrics.fontBoundingBoxDescent ?? 0)
};
};
//# sourceMappingURL=utils.js.map

File diff suppressed because one or more lines are too long