react-native-ui-lib 8.3.4 → 8.4.0-snapshot.7827

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.
@@ -1,10 +1,10 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  project.ext {
4
- buildToolsVersion = rootProject.ext.has("buildToolsVersion") ? rootProject.ext.buildToolsVersion : '35.0.0'
4
+ buildToolsVersion = rootProject.ext.has("buildToolsVersion") ? rootProject.ext.buildToolsVersion : '36.0.0'
5
5
  minSdkVersion = rootProject.ext.has("minSdkVersion") ? rootProject.ext.minSdkVersion : 24
6
- compileSdkVersion = rootProject.ext.has("compileSdkVersion") ? rootProject.ext.compileSdkVersion : 35
7
- targetSdkVersion = rootProject.ext.has("targetSdkVersion") ? rootProject.ext.targetSdkVersion : 34
6
+ compileSdkVersion = rootProject.ext.has("compileSdkVersion") ? rootProject.ext.compileSdkVersion : 36
7
+ targetSdkVersion = rootProject.ext.has("targetSdkVersion") ? rootProject.ext.targetSdkVersion : 36
8
8
  supportLibVersion = rootProject.ext.has("supportLibVersion") ? rootProject.ext.supportLibVersion : '28.0.0'
9
9
  }
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-ui-lib",
3
- "version": "8.3.4",
3
+ "version": "8.4.0-snapshot.7827",
4
4
  "main": "src/index.js",
5
5
  "types": "src/index.d.ts",
6
6
  "author": "Ethan Sharabi <ethan.shar@gmail.com>",
@@ -112,7 +112,6 @@
112
112
  "react-native": ">=0.77.3",
113
113
  "react-native-gesture-handler": ">=2.24.0",
114
114
  "react-native-reanimated": ">=3.17.5",
115
- "react-native-safe-area-context": ">=5.6.2",
116
115
  "uilib-native": "^5.0.1"
117
116
  },
118
117
  "jest": {
@@ -0,0 +1,2 @@
1
+ import {ScreenFooter} from './src';
2
+ export default ScreenFooter;
@@ -0,0 +1 @@
1
+ module.exports = require('./src/components/screenFooter').default;
@@ -9,6 +9,7 @@ const BRANCH_CATEGORIES = [
9
9
  {name: 'fixes', branch: 'fix/', title: ':wrench: Fixes'},
10
10
  {name: 'infra', branch: 'infra/', title: ':gear: Maintenance & Infra'}
11
11
  ];
12
+ const SILENT_PRS = ['none', 'n/a', 'na'];
12
13
 
13
14
  function getBranchPrefixes() {
14
15
  return BRANCH_CATEGORIES.map(category => category.branch);
@@ -95,7 +96,7 @@ function isSilent(pr) {
95
96
  return true;
96
97
  } else {
97
98
  const changelog = pr.info.changelog.toLowerCase();
98
- if (changelog === 'none' || changelog === 'n/a') {
99
+ if (SILENT_PRS.includes(changelog)) {
99
100
  return true;
100
101
  }
101
102
  }
@@ -1,4 +1,7 @@
1
1
  export const images = {
2
+ get bottomGradient() {
3
+ return require('./bottomGradient.png');
4
+ },
2
5
  get gradient() {
3
6
  return require('./gradient.png');
4
7
  },
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { ScreenFooterProps, ScreenFooterLayouts, ScreenFooterBackgrounds, FooterAlignment, HorizontalItemsDistribution, ItemsFit, KeyboardBehavior, ScreenFooterShadow } from './types';
3
+ export { ScreenFooterProps, ScreenFooterLayouts, ScreenFooterBackgrounds, FooterAlignment, HorizontalItemsDistribution, ItemsFit, KeyboardBehavior, ScreenFooterShadow };
4
+ declare const _default: React.ForwardRefExoticComponent<ScreenFooterProps & React.RefAttributes<any>> & {
5
+ (props: ScreenFooterProps): React.JSX.Element;
6
+ displayName: string;
7
+ };
8
+ export default _default;
@@ -0,0 +1,211 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import Animated, { useAnimatedKeyboard, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
4
+ import { Keyboard } from 'uilib-native';
5
+ import { SafeAreaContextPackage } from "../../optionalDependencies";
6
+ import View from "../view";
7
+ import Image from "../image";
8
+ import Assets from "../../assets";
9
+ import { Colors, Shadows, Spacings } from "../../style";
10
+ import { asBaseComponent, Constants } from "../../commons/new";
11
+ import { useKeyboardHeight } from "../../hooks";
12
+ import { ScreenFooterProps, ScreenFooterLayouts, ScreenFooterBackgrounds, FooterAlignment, HorizontalItemsDistribution, ItemsFit, KeyboardBehavior, ScreenFooterShadow } from "./types";
13
+ export { ScreenFooterProps, ScreenFooterLayouts, ScreenFooterBackgrounds, FooterAlignment, HorizontalItemsDistribution, ItemsFit, KeyboardBehavior, ScreenFooterShadow };
14
+ const ScreenFooter = props => {
15
+ const {
16
+ testID,
17
+ layout,
18
+ alignment,
19
+ horizontalAlignment,
20
+ backgroundType,
21
+ children,
22
+ keyboardBehavior = KeyboardBehavior.STICKY,
23
+ itemsFit,
24
+ itemWidth,
25
+ horizontalItemsDistribution: distribution,
26
+ visible = true,
27
+ animationDuration = 200,
28
+ shadow = ScreenFooterShadow.SH20,
29
+ hideDivider = false
30
+ } = props;
31
+ const keyboard = useAnimatedKeyboard();
32
+ const [height, setHeight] = useState(0);
33
+ const visibilityTranslateY = useSharedValue(0);
34
+
35
+ // Update visibility translation when visible or height changes
36
+ useEffect(() => {
37
+ visibilityTranslateY.value = withTiming(visible ? 0 : height, {
38
+ duration: animationDuration
39
+ });
40
+ }, [visible, height, animationDuration, visibilityTranslateY]);
41
+
42
+ // Animated style for STICKY behavior (counters Android system offset + visibility)
43
+ const stickyAnimatedStyle = useAnimatedStyle(() => {
44
+ const counterSystemOffset = Constants.isAndroid ? keyboard.height.value : 0;
45
+ return {
46
+ transform: [{
47
+ translateY: counterSystemOffset + visibilityTranslateY.value
48
+ }]
49
+ };
50
+ });
51
+
52
+ // Animated style for HOISTED behavior (visibility only, keyboard handled by KeyboardAccessoryView)
53
+ const hoistedAnimatedStyle = useAnimatedStyle(() => {
54
+ return {
55
+ transform: [{
56
+ translateY: visibilityTranslateY.value
57
+ }]
58
+ };
59
+ });
60
+ const onLayout = useCallback(event => {
61
+ setHeight(event.nativeEvent.layout.height);
62
+ }, []);
63
+ const isSolid = backgroundType === ScreenFooterBackgrounds.SOLID;
64
+ const isFading = backgroundType === ScreenFooterBackgrounds.FADING;
65
+ const isHorizontal = layout === ScreenFooterLayouts.HORIZONTAL;
66
+ const childrenCount = React.Children.count(children);
67
+ const justifyContent = useMemo(() => {
68
+ if (isHorizontal) {
69
+ if (distribution === HorizontalItemsDistribution.SPREAD) {
70
+ return childrenCount === 1 ? 'center' : 'space-between';
71
+ }
72
+ switch (horizontalAlignment) {
73
+ case FooterAlignment.START:
74
+ return 'flex-start';
75
+ case FooterAlignment.END:
76
+ return 'flex-end';
77
+ default:
78
+ return 'center';
79
+ }
80
+ }
81
+ return 'flex-start';
82
+ }, [isHorizontal, distribution, horizontalAlignment, childrenCount]);
83
+ const alignItems = useMemo(() => {
84
+ if (layout === ScreenFooterLayouts.VERTICAL) {
85
+ if (itemsFit === ItemsFit.STRETCH) {
86
+ return 'stretch';
87
+ }
88
+ }
89
+ switch (alignment) {
90
+ case FooterAlignment.START:
91
+ return 'flex-start';
92
+ case FooterAlignment.END:
93
+ return 'flex-end';
94
+ default:
95
+ return 'center';
96
+ }
97
+ }, [layout, itemsFit, alignment]);
98
+ const useSafeAreaInsets = SafeAreaContextPackage?.useSafeAreaInsets ?? (() => Constants.getSafeAreaInsets());
99
+ const insets = useSafeAreaInsets();
100
+ const keyboardHeight = useKeyboardHeight();
101
+ const isKeyboardVisible = keyboardHeight > 0;
102
+ const contentContainerStyle = useMemo(() => {
103
+ const style = [styles.contentContainer, layout === ScreenFooterLayouts.HORIZONTAL ? styles.horizontalContainer : styles.verticalContainer, {
104
+ alignItems,
105
+ justifyContent
106
+ }];
107
+ if (!isKeyboardVisible) {
108
+ style.push({
109
+ paddingBottom: insets.bottom
110
+ });
111
+ }
112
+ if (isSolid) {
113
+ const shadowStyle = Shadows[shadow]?.top;
114
+ const backgroundElevation = shadowStyle?.elevation || 0;
115
+ // When the background has a shadow (elevation on Android), it might render on top of the content
116
+ style.push({
117
+ elevation: backgroundElevation + 1
118
+ });
119
+ }
120
+ return style;
121
+ }, [layout, alignItems, justifyContent, insets.bottom, isSolid, shadow, isKeyboardVisible]);
122
+ const solidBackgroundStyle = useMemo(() => {
123
+ if (!isSolid) {
124
+ return undefined;
125
+ }
126
+ const shadowStyle = Shadows[shadow]?.top;
127
+ const dividerStyle = hideDivider ? undefined : {
128
+ borderTopWidth: 1,
129
+ borderColor: Colors.$outlineDefault
130
+ };
131
+ return [shadowStyle, dividerStyle];
132
+ }, [isSolid, shadow, hideDivider]);
133
+ const renderBackground = useCallback(() => {
134
+ if (isSolid) {
135
+ return <View testID={testID ? `${testID}.solidBackground` : undefined} absF bg-$backgroundElevated style={solidBackgroundStyle} pointerEvents="none" />;
136
+ }
137
+ if (isFading) {
138
+ return <View testID={testID ? `${testID}.fadingBackground` : undefined} absF pointerEvents="none">
139
+ <Image source={Assets.internal.images.bottomGradient} style={styles.background} resizeMode="stretch" tintColor={Colors.$backgroundDefault} />
140
+ </View>;
141
+ }
142
+ return null;
143
+ }, [testID, isSolid, isFading, solidBackgroundStyle]);
144
+ const renderChild = useCallback((child, index) => {
145
+ if (itemsFit === ItemsFit.FIXED && itemWidth) {
146
+ const fixedStyle = isHorizontal ? {
147
+ width: itemWidth,
148
+ flexShrink: 1,
149
+ overflow: 'hidden',
150
+ flexDirection: 'row',
151
+ justifyContent: 'center'
152
+ } : {
153
+ width: itemWidth,
154
+ maxWidth: '100%'
155
+ };
156
+ return <View key={index} style={fixedStyle}>
157
+ {child}
158
+ </View>;
159
+ }
160
+ if (isHorizontal && React.isValidElement(child) && itemsFit === ItemsFit.STRETCH) {
161
+ return <View flex row centerH key={index}>
162
+ {child}
163
+ </View>;
164
+ }
165
+ return child;
166
+ }, [itemsFit, itemWidth, isHorizontal]);
167
+ const childrenArray = React.Children.toArray(children).slice(0, 3).map(renderChild);
168
+ const renderFooterContent = useCallback(() => {
169
+ return <>
170
+ {renderBackground()}
171
+ <View testID={testID ? `${testID}.content` : undefined} style={contentContainerStyle}>
172
+ {childrenArray}
173
+ </View>
174
+ </>;
175
+ }, [renderBackground, testID, contentContainerStyle, childrenArray]);
176
+ if (keyboardBehavior === KeyboardBehavior.HOISTED) {
177
+ return <Animated.View style={[styles.container, hoistedAnimatedStyle]} pointerEvents={visible ? 'box-none' : 'none'}>
178
+ <Keyboard.KeyboardAccessoryView renderContent={renderFooterContent} kbInputRef={undefined} scrollBehavior={Keyboard.KeyboardAccessoryView.scrollBehaviors.FIXED_OFFSET} useSafeArea={false} manageScrollView={false} revealKeyboardInteractive onHeightChanged={setHeight} />
179
+ </Animated.View>;
180
+ }
181
+ return <Animated.View testID={testID} onLayout={onLayout} style={[styles.container, stickyAnimatedStyle]}>
182
+ {renderFooterContent()}
183
+ </Animated.View>;
184
+ };
185
+ ScreenFooter.displayName = 'ScreenFooter';
186
+ const styles = StyleSheet.create({
187
+ container: {
188
+ position: 'absolute',
189
+ bottom: 0,
190
+ left: 0,
191
+ right: 0
192
+ },
193
+ contentContainer: {
194
+ paddingTop: Spacings.s4,
195
+ paddingHorizontal: Spacings.s5,
196
+ paddingBottom: Spacings.s5
197
+ },
198
+ horizontalContainer: {
199
+ flexDirection: 'row',
200
+ gap: Spacings.s5
201
+ },
202
+ verticalContainer: {
203
+ flexDirection: 'column',
204
+ gap: Spacings.s3
205
+ },
206
+ background: {
207
+ width: '100%',
208
+ height: '100%'
209
+ }
210
+ });
211
+ export default asBaseComponent(ScreenFooter);
@@ -0,0 +1,262 @@
1
+ {
2
+ "name": "ScreenFooter",
3
+ "category": "layout",
4
+ "description": "A flexible footer component that stays at the bottom of the screen with support for various layouts, backgrounds, and keyboard behaviors",
5
+ "example": "https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/ScreenFooterScreen.tsx",
6
+ "images": [],
7
+ "props": [
8
+ {
9
+ "name": "backgroundType",
10
+ "type": "ScreenFooterBackgrounds",
11
+ "description": "The background style of the footer [fading, solid, transparent]",
12
+ "default": "'solid'"
13
+ },
14
+ {
15
+ "name": "layout",
16
+ "type": "ScreenFooterLayouts",
17
+ "description": "The layout direction of footer items [horizontal, vertical]",
18
+ "default": "'vertical'"
19
+ },
20
+ {
21
+ "name": "alignment",
22
+ "type": "FooterAlignment",
23
+ "description": "Cross-axis alignment [start, center, end]. In vertical layout: controls horizontal position. In horizontal layout: controls vertical position",
24
+ "default": "'center'"
25
+ },
26
+ {
27
+ "name": "horizontalAlignment",
28
+ "type": "FooterAlignment",
29
+ "description": "Main-axis alignment for horizontal layout only when distribution is STACK [start, center, end]",
30
+ "default": "'center'"
31
+ },
32
+ {
33
+ "name": "horizontalItemsDistribution",
34
+ "type": "HorizontalItemsDistribution",
35
+ "description": "Distribution of items in horizontal layout [stack, spread]",
36
+ "default": "'spread'"
37
+ },
38
+ {
39
+ "name": "itemsFit",
40
+ "type": "ItemsFit",
41
+ "description": "How items should fit in vertical layout [fit, fixed, stretch]",
42
+ "default": "'fit'"
43
+ },
44
+ {
45
+ "name": "keyboardBehavior",
46
+ "type": "KeyboardBehavior",
47
+ "description": "The footer's keyboard behavior [sticky, hoisted]. Sticky: stays at bottom when keyboard opens. Hoisted: pushed up when keyboard opens",
48
+ "default": "'sticky'"
49
+ },
50
+ {
51
+ "name": "itemWidth",
52
+ "type": "DimensionValue",
53
+ "description": "Fixed width for all items (used with ItemsFit.FIXED)"
54
+ },
55
+ {
56
+ "name": "visible",
57
+ "type": "boolean",
58
+ "description": "If true, the footer is visible. If false, it slides down",
59
+ "default": "true"
60
+ },
61
+ {
62
+ "name": "animationDuration",
63
+ "type": "number",
64
+ "description": "Duration of the show/hide animation in ms",
65
+ "default": "200"
66
+ },
67
+ {
68
+ "name": "useSafeArea",
69
+ "type": "boolean",
70
+ "description": "If true, the footer will respect the safe area (add bottom padding)",
71
+ "default": "true"
72
+ },
73
+ {
74
+ "name": "shadow",
75
+ "type": "ScreenFooterShadow",
76
+ "description": "Shadow preset for solid background [sh10, sh20, sh30]. Only applies when backgroundType is 'solid'",
77
+ "default": "'sh20'"
78
+ },
79
+ {
80
+ "name": "hideDivider",
81
+ "type": "boolean",
82
+ "description": "If true, hides the top divider for solid background. Only applies when backgroundType is 'solid'",
83
+ "default": "false"
84
+ }
85
+ ],
86
+ "snippet": [
87
+ "<View flex bg-$backgroundDefault>",
88
+ " <ScrollView>",
89
+ " {/* Screen content */}",
90
+ " </ScrollView>",
91
+ " <ScreenFooter>",
92
+ " <Button label=\"Primary Action\" />",
93
+ " <Button label=\"Secondary Action\" link />",
94
+ " </ScreenFooter>",
95
+ "</View>"
96
+ ],
97
+ "docs": {
98
+ "hero": {
99
+ "title": "ScreenFooter",
100
+ "description": "The ScreenFooter component provides a flexible footer that stays at the bottom of the screen with support for various layouts, backgrounds, and keyboard behaviors. It's ideal for forms, checkout flows, and any screen requiring persistent bottom actions.",
101
+ "type": "hero",
102
+ "layout": "horizontal",
103
+ "content": [
104
+ {
105
+ "value": "https://wixmp-1d257fba8470f1b562a0f5f2.wixmp.com/mads-docs-assets/assets/Components%20Docs/ScreenFooter/screenFooter_overview_preview.png"
106
+ }
107
+ ]
108
+ },
109
+ "tabs": [
110
+ {
111
+ "title": "Overview",
112
+ "sections": [
113
+ {
114
+ "type": "section",
115
+ "title": "Use Examples",
116
+ "description": "The ScreenFooter can be used in various scenarios depending on the content and action hierarchy. Each layout type serves different use cases and provides different visual emphasis.",
117
+ "content": []
118
+ },
119
+ {
120
+ "type": "table",
121
+ "columns": [
122
+ "Layout Type",
123
+ "Use Case",
124
+ "Example"
125
+ ],
126
+ "items": [
127
+ {
128
+ "title": "Horizontal - Spread",
129
+ "content": [
130
+ {
131
+ "value": "https://wixmp-1d257fba8470f1b562a0f5f2.wixmp.com/mads-docs-assets/assets/Components%20Docs/ScreenFooter/screenFooter_horizontal_spread.png"
132
+ }
133
+ ],
134
+ "description": "Best for two equally important actions positioned at opposite ends. Common in navigation flows or when presenting contrasting choices."
135
+ },
136
+ {
137
+ "title": "Horizontal - Stacked Center",
138
+ "content": [
139
+ {
140
+ "value": "https://wixmp-1d257fba8470f1b562a0f5f2.wixmp.com/mads-docs-assets/assets/Components%20Docs/ScreenFooter/screenFooter_horizontal_stacked.png"
141
+ }
142
+ ],
143
+ "description": "Ideal for primary and secondary actions of similar importance that should be grouped together in the center."
144
+ },
145
+ {
146
+ "title": "Horizontal - With Content",
147
+ "content": [
148
+ {
149
+ "value": "https://wixmp-1d257fba8470f1b562a0f5f2.wixmp.com/mads-docs-assets/assets/Components%20Docs/ScreenFooter/screenFooter_horizontal_content.png"
150
+ }
151
+ ],
152
+ "description": "Perfect for checkout or summary screens where you need to display information alongside actions. The footer can accommodate text, prices, or other content elements."
153
+ }
154
+ ],
155
+ "title": "Horizontal Layouts",
156
+ "description": "markdown: Horizontal layouts are best when screen content is abundant and vertical space is limited. They keep actions compact while maintaining good reachability. Use the `layout` prop set to `ScreenFooterLayouts.HORIZONTAL`."
157
+ },
158
+ {
159
+ "type": "table",
160
+ "columns": [
161
+ "Layout Type",
162
+ "Use Case",
163
+ "Example"
164
+ ],
165
+ "items": [
166
+ {
167
+ "title": "Vertical - Fit",
168
+ "content": [
169
+ {
170
+ "value": "https://wixmp-1d257fba8470f1b562a0f5f2.wixmp.com/mads-docs-assets/assets/Components%20Docs/ScreenFooter/screenFooter_vertical_fit.png"
171
+ }
172
+ ],
173
+ "description": "Buttons are sized based on their content. Best for clear action hierarchy with a prominent primary button and a subtle secondary link button."
174
+ },
175
+ {
176
+ "title": "Vertical - Stretched",
177
+ "content": [
178
+ {
179
+ "value": "https://wixmp-1d257fba8470f1b562a0f5f2.wixmp.com/mads-docs-assets/assets/Components%20Docs/ScreenFooter/screenFooter_vertical_stretched.png"
180
+ }
181
+ ],
182
+ "description": "Full-width buttons provide maximum tap area and visual prominence. Recommended for forms and high-priority actions where accessibility is crucial."
183
+ }
184
+ ],
185
+ "title": "Vertical Layouts",
186
+ "description": "markdown: Vertical layouts provide better action prominence and are recommended when a single primary action needs emphasis, or when content can be obscured. Use `itemsFit` prop with `ItemsFit.STRETCH` for full-width buttons or `ItemsFit.FIT` for content-sized buttons."
187
+ },
188
+ {
189
+ "type": "table",
190
+ "columns": [
191
+ "Background Type",
192
+ "Visual",
193
+ "When to Use"
194
+ ],
195
+ "items": [
196
+ {
197
+ "title": "Solid",
198
+ "content": [
199
+ {
200
+ "value": "https://wixmp-1d257fba8470f1b562a0f5f2.wixmp.com/mads-docs-assets/assets/Components%20Docs/ScreenFooter/screenFooter_background_solid.png"
201
+ }
202
+ ],
203
+ "description": "Provides clear separation with a solid background and optional shadow. Best for content-heavy screens where the footer needs to stand out."
204
+ },
205
+ {
206
+ "title": "Fading",
207
+ "content": [
208
+ {
209
+ "value": "https://wixmp-1d257fba8470f1b562a0f5f2.wixmp.com/mads-docs-assets/assets/Components%20Docs/ScreenFooter/screenFooter_background_fading.png"
210
+ }
211
+ ],
212
+ "description": "Creates a subtle gradient overlay that doesn't obscure content completely. Ideal for minimal designs or when content should remain partially visible."
213
+ },
214
+ {
215
+ "title": "Transparent",
216
+ "content": [
217
+ {
218
+ "value": "https://wixmp-1d257fba8470f1b562a0f5f2.wixmp.com/mads-docs-assets/assets/Components%20Docs/ScreenFooter/screenFooter_background_transparent.png"
219
+ }
220
+ ],
221
+ "description": "No background overlay. Use when the screen has minimal content or a solid background color, and separation isn't needed."
222
+ }
223
+ ],
224
+ "title": "Background Types",
225
+ "description": "markdown: Choose the background type based on your content density and visual hierarchy needs. Set using the `backgroundType` prop."
226
+ },
227
+ {
228
+ "type": "table",
229
+ "columns": [
230
+ "Behavior",
231
+ "Description",
232
+ "Example"
233
+ ],
234
+ "items": [
235
+ {
236
+ "title": "Sticky",
237
+ "content": [
238
+ {
239
+ "value": "https://wixmp-1d257fba8470f1b562a0f5f2.wixmp.com/mads-docs-assets/assets/Components%20Docs/ScreenFooter/screenFooter_behavior_sticky.png"
240
+ }
241
+ ],
242
+ "description": "The footer remains at the bottom of the screen even when the keyboard is open. Best for short forms or when the footer actions don't relate to keyboard input."
243
+ },
244
+ {
245
+ "title": "Hoisted",
246
+ "content": [
247
+ {
248
+ "value": "https://wixmp-1d257fba8470f1b562a0f5f2.wixmp.com/mads-docs-assets/assets/Components%20Docs/ScreenFooter/screenFooter_behavior_hoisted.png"
249
+ }
250
+ ],
251
+ "description": "The footer moves up with the keyboard, staying above it. Recommended for forms where users need immediate access to submit actions while typing."
252
+ }
253
+ ],
254
+ "title": "Keyboard Behavior",
255
+ "description": "markdown: Control how the footer responds to the keyboard using the `keyboardBehavior` prop. Choose `KeyboardBehavior.STICKY` to keep it at the bottom, or `KeyboardBehavior.HOISTED` to move it above the keyboard."
256
+ }
257
+ ]
258
+ }
259
+ ]
260
+ }
261
+ }
262
+
@@ -0,0 +1,100 @@
1
+ import { PropsWithChildren } from 'react';
2
+ import { DimensionValue } from 'react-native';
3
+ export declare enum ScreenFooterLayouts {
4
+ HORIZONTAL = "horizontal",
5
+ VERTICAL = "vertical"
6
+ }
7
+ export declare enum ScreenFooterBackgrounds {
8
+ FADING = "fading",
9
+ SOLID = "solid",
10
+ TRANSPARENT = "transparent"
11
+ }
12
+ export declare enum FooterAlignment {
13
+ START = "start",
14
+ CENTER = "center",
15
+ END = "end"
16
+ }
17
+ export declare enum HorizontalItemsDistribution {
18
+ STACK = "stack",
19
+ SPREAD = "spread"
20
+ }
21
+ export declare enum ItemsFit {
22
+ FIT = "fit",
23
+ STRETCH = "stretch",
24
+ FIXED = "fixed"
25
+ }
26
+ export declare enum KeyboardBehavior {
27
+ STICKY = "sticky",
28
+ HOISTED = "hoisted"
29
+ }
30
+ export declare enum ScreenFooterShadow {
31
+ SH10 = "sh10",
32
+ SH20 = "sh20",
33
+ SH30 = "sh30"
34
+ }
35
+ export interface ScreenFooterProps extends PropsWithChildren<{}> {
36
+ /**
37
+ * Used as testing identifier
38
+ */
39
+ testID?: string;
40
+ /**
41
+ * The background style of the footer
42
+ */
43
+ backgroundType?: ScreenFooterBackgrounds | `${ScreenFooterBackgrounds}`;
44
+ /**
45
+ * The layout direction of footer items
46
+ */
47
+ layout?: ScreenFooterLayouts | `${ScreenFooterLayouts}`;
48
+ /**
49
+ * Cross-axis alignment:
50
+ * - Vertical layout: controls horizontal position (left/center/right)
51
+ * - Horizontal layout: controls vertical position (top/center/bottom)
52
+ */
53
+ alignment?: FooterAlignment | `${FooterAlignment}`;
54
+ /**
55
+ * Main-axis alignment for horizontal layout only (when distribution is STACK):
56
+ * Controls horizontal position (left/center/right) of the stacked items
57
+ */
58
+ horizontalAlignment?: FooterAlignment | `${FooterAlignment}`;
59
+ /**
60
+ * Distribution of items in horizontal layout (stack/spread)
61
+ */
62
+ horizontalItemsDistribution?: HorizontalItemsDistribution | `${HorizontalItemsDistribution}`;
63
+ /**
64
+ * How items should fit in vertical layout (fit/fixed/stretch)
65
+ */
66
+ itemsFit?: ItemsFit | `${ItemsFit}`;
67
+ /**
68
+ * The footer's keyboard behavior.
69
+ * When STICKY, the footer will stay at the bottom of the screen when keyboard is opened.
70
+ * When HOISTED, the footer will be pushed up when keyboard is opened.
71
+ */
72
+ keyboardBehavior?: KeyboardBehavior | `${KeyboardBehavior}`;
73
+ /**
74
+ * Fixed width for all items (used with ItemsFit.FIXED)
75
+ */
76
+ itemWidth?: DimensionValue;
77
+ /**
78
+ * If true, the footer is visible. If false, it slides down.
79
+ */
80
+ visible?: boolean;
81
+ /**
82
+ * Duration of the show/hide animation in ms.
83
+ * @default 200
84
+ */
85
+ animationDuration?: number;
86
+ /**
87
+ * If true, the footer will respect the safe area (add bottom padding)
88
+ */
89
+ useSafeArea?: boolean;
90
+ /**
91
+ * Shadow preset for solid background (default: SH20)
92
+ * Only applies when backgroundType is 'solid'
93
+ */
94
+ shadow?: ScreenFooterShadow | `${ScreenFooterShadow}`;
95
+ /**
96
+ * If true, hides the top divider for solid background (default: false)
97
+ * Only applies when backgroundType is 'solid'
98
+ */
99
+ hideDivider?: boolean;
100
+ }
@@ -0,0 +1,39 @@
1
+ export let ScreenFooterLayouts = /*#__PURE__*/function (ScreenFooterLayouts) {
2
+ ScreenFooterLayouts["HORIZONTAL"] = "horizontal";
3
+ ScreenFooterLayouts["VERTICAL"] = "vertical";
4
+ return ScreenFooterLayouts;
5
+ }({});
6
+ export let ScreenFooterBackgrounds = /*#__PURE__*/function (ScreenFooterBackgrounds) {
7
+ ScreenFooterBackgrounds["FADING"] = "fading";
8
+ ScreenFooterBackgrounds["SOLID"] = "solid";
9
+ ScreenFooterBackgrounds["TRANSPARENT"] = "transparent";
10
+ return ScreenFooterBackgrounds;
11
+ }({});
12
+ export let FooterAlignment = /*#__PURE__*/function (FooterAlignment) {
13
+ FooterAlignment["START"] = "start";
14
+ FooterAlignment["CENTER"] = "center";
15
+ FooterAlignment["END"] = "end";
16
+ return FooterAlignment;
17
+ }({});
18
+ export let HorizontalItemsDistribution = /*#__PURE__*/function (HorizontalItemsDistribution) {
19
+ HorizontalItemsDistribution["STACK"] = "stack";
20
+ HorizontalItemsDistribution["SPREAD"] = "spread";
21
+ return HorizontalItemsDistribution;
22
+ }({});
23
+ export let ItemsFit = /*#__PURE__*/function (ItemsFit) {
24
+ ItemsFit["FIT"] = "fit";
25
+ ItemsFit["STRETCH"] = "stretch";
26
+ ItemsFit["FIXED"] = "fixed";
27
+ return ItemsFit;
28
+ }({});
29
+ export let KeyboardBehavior = /*#__PURE__*/function (KeyboardBehavior) {
30
+ KeyboardBehavior["STICKY"] = "sticky";
31
+ KeyboardBehavior["HOISTED"] = "hoisted";
32
+ return KeyboardBehavior;
33
+ }({});
34
+ export let ScreenFooterShadow = /*#__PURE__*/function (ScreenFooterShadow) {
35
+ ScreenFooterShadow["SH10"] = "sh10";
36
+ ScreenFooterShadow["SH20"] = "sh20";
37
+ ScreenFooterShadow["SH30"] = "sh30";
38
+ return ScreenFooterShadow;
39
+ }({});
@@ -23,6 +23,7 @@ declare const _default: React.ForwardRefExoticComponent<Omit<import("./Thumb").T
23
23
  disableRTL?: boolean | undefined;
24
24
  accessible?: boolean | undefined;
25
25
  testID?: string | undefined;
26
+ useRelativeDrag?: boolean | undefined;
26
27
  migrate?: boolean | undefined;
27
28
  enableThumbShadow?: boolean | undefined;
28
29
  disabledThumbTintColor?: string | undefined;
@@ -1,4 +1,5 @@
1
1
  import _isFunction from "lodash/isFunction";
2
+ import _clamp from "lodash/clamp";
2
3
  import React, { PureComponent } from 'react';
3
4
  import { StyleSheet, PanResponder, AccessibilityInfo, Animated } from 'react-native';
4
5
  import { Constants, asBaseComponent } from "../../commons/new";
@@ -55,6 +56,7 @@ class Slider extends PureComponent {
55
56
  width: THUMB_SIZE,
56
57
  height: THUMB_SIZE
57
58
  };
59
+ _containerDragInitialValue = 0;
58
60
  constructor(props) {
59
61
  super(props);
60
62
  this.activeThumbRef = this.thumb;
@@ -85,6 +87,15 @@ class Slider extends PureComponent {
85
87
  onPanResponderEnd: () => true,
86
88
  onPanResponderTerminationRequest: () => false
87
89
  });
90
+ this.containerPanResponder = PanResponder.create({
91
+ onMoveShouldSetPanResponder: this.handleMoveShouldSetPanResponder,
92
+ onPanResponderGrant: this.handleContainerGrant,
93
+ onPanResponderMove: this.handleContainerMove,
94
+ onPanResponderRelease: this.handleContainerEnd,
95
+ onStartShouldSetPanResponder: () => true,
96
+ onPanResponderEnd: () => true,
97
+ onPanResponderTerminationRequest: () => false
98
+ });
88
99
  }
89
100
  reset() {
90
101
  // NOTE: used with ref
@@ -203,6 +214,32 @@ class Slider extends PureComponent {
203
214
  this.bounceToStep();
204
215
  this.onSeekEnd();
205
216
  };
217
+ handleContainerGrant = () => {
218
+ this._containerDragInitialValue = this.getValueForX(this._x);
219
+ this.onSeekStart();
220
+ };
221
+ handleContainerMove = (_e, gestureState) => {
222
+ if (this.props.disabled) {
223
+ return;
224
+ }
225
+ const {
226
+ minimumValue,
227
+ maximumValue
228
+ } = this.props;
229
+ const range = this.getRange();
230
+ const containerWidth = this.state.containerSize.width;
231
+ const dx = gestureState.dx * (Constants.isRTL && !this.disableRTL ? -1 : 1);
232
+ const deltaValue = dx / containerWidth * range;
233
+ const newValue = _clamp(this._containerDragInitialValue + deltaValue, minimumValue, maximumValue);
234
+ const newX = this.getXForValue(newValue);
235
+ this.set_x(newX);
236
+ this.moveTo(newX);
237
+ this.updateValue(newX);
238
+ };
239
+ handleContainerEnd = () => {
240
+ this.bounceToStep();
241
+ this.onSeekEnd();
242
+ };
206
243
 
207
244
  /* Actions */
208
245
 
@@ -463,6 +500,9 @@ class Slider extends PureComponent {
463
500
  onThumbLayout = nativeEvent => {
464
501
  this.handleMeasure('thumbSize', nativeEvent);
465
502
  };
503
+ handleContainerShouldSetResponder = () => {
504
+ return !this.props.disabled;
505
+ };
466
506
  handleTrackPress = event => {
467
507
  if (this.props.disabled) {
468
508
  return;
@@ -562,10 +602,16 @@ class Slider extends PureComponent {
562
602
 
563
603
  /* Renders */
564
604
  renderMinThumb = () => {
565
- return <Thumb {...this.getThumbProps()} ref={this.minThumb} onTouchStart={this.onMinTouchStart} {...this.panResponder.panHandlers} />;
605
+ const {
606
+ useRelativeDrag
607
+ } = this.props;
608
+ return <Thumb {...this.getThumbProps()} ref={this.minThumb} onTouchStart={useRelativeDrag ? undefined : this.onMinTouchStart} pointerEvents={useRelativeDrag ? 'none' : undefined} {...useRelativeDrag ? {} : this.panResponder.panHandlers} />;
566
609
  };
567
610
  renderThumb = () => {
568
- return <Thumb {...this.getThumbProps()} ref={this.thumb} onTouchStart={this.onTouchStart} {...this.panResponder.panHandlers} />;
611
+ const {
612
+ useRelativeDrag
613
+ } = this.props;
614
+ return <Thumb {...this.getThumbProps()} ref={this.thumb} onTouchStart={useRelativeDrag ? undefined : this.onTouchStart} pointerEvents={useRelativeDrag ? 'none' : undefined} {...useRelativeDrag ? {} : this.panResponder.panHandlers} />;
569
615
  };
570
616
  renderTrack() {
571
617
  const {
@@ -607,14 +653,18 @@ class Slider extends PureComponent {
607
653
  const {
608
654
  containerStyle,
609
655
  testID,
610
- migrate
656
+ migrate,
657
+ useRelativeDrag
611
658
  } = this.props;
612
659
  if (migrate) {
613
660
  return <IncubatorSlider {...this.props} />;
614
661
  }
615
- return <View style={[styles.container, containerStyle]} onLayout={this.onContainerLayout} onAccessibilityAction={this.onAccessibilityAction} testID={testID} {...this.getAccessibilityProps()}>
662
+ const containerGestureProps = useRelativeDrag ? this.containerPanResponder.panHandlers : {
663
+ onStartShouldSetResponder: this.handleContainerShouldSetResponder,
664
+ onResponderRelease: this.handleTrackPress
665
+ };
666
+ return <View style={[styles.container, containerStyle]} onLayout={this.onContainerLayout} onAccessibilityAction={this.onAccessibilityAction} testID={testID} {...containerGestureProps} {...this.getAccessibilityProps()}>
616
667
  {this.renderTrack()}
617
- <View style={styles.touchArea} onTouchEnd={this.handleTrackPress} />
618
668
  {this.renderRangeThumb()}
619
669
  {this.renderThumb()}
620
670
  </View>;
@@ -636,9 +686,5 @@ const styles = StyleSheet.create({
636
686
  },
637
687
  trackDisableRTL: {
638
688
  right: 0
639
- },
640
- touchArea: {
641
- ...StyleSheet.absoluteFillObject,
642
- backgroundColor: 'transparent'
643
689
  }
644
690
  });
@@ -140,6 +140,12 @@
140
140
  "type": "string",
141
141
  "description": "The component test id"
142
142
  },
143
+ {
144
+ "name": "useRelativeDrag",
145
+ "type": "boolean",
146
+ "description": "If true, dragging anywhere on the slider moves the thumb relative to its current position instead of snapping to the touch point. Designed for single-thumb mode.",
147
+ "default": "false"
148
+ },
143
149
  {
144
150
  "name": "migrate",
145
151
  "type": "boolean",
@@ -97,6 +97,11 @@ export type SliderProps = Omit<ThumbProps, 'ref'> & {
97
97
  * The slider's test identifier
98
98
  */
99
99
  testID?: string;
100
+ /**
101
+ * If true, dragging anywhere on the slider moves the thumb relative to its current position
102
+ * instead of snapping to the touch point. Designed for single-thumb mode.
103
+ */
104
+ useRelativeDrag?: boolean;
100
105
  /**
101
106
  * Whether to use the new Slider implementation using Reanimated
102
107
  */
@@ -6,6 +6,7 @@ export { default as useModifiers } from './useModifiers';
6
6
  export { default as useOrientation } from './useOrientation';
7
7
  export { default as useScrollEnabler } from './useScrollEnabler';
8
8
  export { default as useScrollReached } from './useScrollReached';
9
+ export { default as useScrollToHide } from './useScrollToHide';
9
10
  export { default as useScrollToItem } from './useScrollToItem';
10
11
  export { default as useScrollTo } from './useScrollTo';
11
12
  export { default as useThemeProps } from './useThemeProps';
@@ -6,6 +6,7 @@ export { default as useModifiers } from "./useModifiers";
6
6
  export { default as useOrientation } from "./useOrientation";
7
7
  export { default as useScrollEnabler } from "./useScrollEnabler";
8
8
  export { default as useScrollReached } from "./useScrollReached";
9
+ export { default as useScrollToHide } from "./useScrollToHide";
9
10
  export { default as useScrollToItem } from "./useScrollToItem";
10
11
  export { default as useScrollTo } from "./useScrollTo";
11
12
  export { default as useThemeProps } from "./useThemeProps";
@@ -0,0 +1,24 @@
1
+ import { NativeSyntheticEvent, NativeScrollEvent } from 'react-native';
2
+ export type ScrollToHideProps = {
3
+ /**
4
+ * The threshold (in pixels) to ignore small scroll movements before updating visibility.
5
+ * Defaults to 0 (immediate reaction).
6
+ */
7
+ scrollingThreshold?: number;
8
+ };
9
+ export type ScrollToHideResult = {
10
+ /**
11
+ * onScroll callback (should be passed to your ScrollView/FlatList onScroll prop).
12
+ */
13
+ onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
14
+ /**
15
+ * Whether the footer should be visible based on scroll direction.
16
+ */
17
+ visible: boolean;
18
+ };
19
+ /**
20
+ * @description: A hook that tracks scroll direction to toggle visibility (e.g., for hiding a footer on scroll).
21
+ * @example: const {onScroll, visible} = useScrollToHide();
22
+ */
23
+ declare const useScrollToHide: (props?: ScrollToHideProps) => ScrollToHideResult;
24
+ export default useScrollToHide;
@@ -0,0 +1,48 @@
1
+ import { useState, useCallback, useRef } from 'react';
2
+ /**
3
+ * @description: A hook that tracks scroll direction to toggle visibility (e.g., for hiding a footer on scroll).
4
+ * @example: const {onScroll, visible} = useScrollToHide();
5
+ */
6
+ const useScrollToHide = (props = {}) => {
7
+ const {
8
+ scrollingThreshold = 0
9
+ } = props;
10
+ const [visible, setVisible] = useState(true);
11
+ const prevContentOffset = useRef(0);
12
+ const onScroll = useCallback(event => {
13
+ const {
14
+ nativeEvent: {
15
+ contentOffset: {
16
+ y: currentOffset
17
+ },
18
+ contentSize: {
19
+ height: contentHeight
20
+ },
21
+ layoutMeasurement: {
22
+ height: layoutHeight
23
+ }
24
+ }
25
+ } = event;
26
+
27
+ // Ignore bounces (iOS)
28
+ if (currentOffset < 0 || currentOffset > contentHeight - layoutHeight) {
29
+ return;
30
+ }
31
+ const diff = currentOffset - prevContentOffset.current;
32
+ if (Math.abs(diff) > scrollingThreshold) {
33
+ if (diff > 0 && visible) {
34
+ // Scrolling Down -> Hide
35
+ setVisible(false);
36
+ } else if (diff < 0 && !visible) {
37
+ // Scrolling Up -> Show
38
+ setVisible(true);
39
+ }
40
+ prevContentOffset.current = currentOffset;
41
+ }
42
+ }, [visible, scrollingThreshold]);
43
+ return {
44
+ onScroll,
45
+ visible
46
+ };
47
+ };
48
+ export default useScrollToHide;
@@ -33,11 +33,15 @@ const ExpandableOverlay = (props, ref) => {
33
33
  setExpandableVisible(true);
34
34
  onPress?.(props);
35
35
  }, [onPress, customValue]);
36
- const closeExpandable = useCallback(() => {
36
+ const dismissOverlay = useCallback(() => {
37
37
  setExpandableVisible(false);
38
38
  focusAccessibility();
39
+ }, [focusAccessibility]);
40
+ const closeExpandable = useCallback(() => {
41
+ dismissOverlay();
39
42
  useDialog ? dialogProps?.onDismiss?.() : modalProps?.onDismiss?.();
40
- }, [useDialog, dialogProps?.onDismiss, modalProps?.onDismiss, focusAccessibility]);
43
+ // eslint-disable-next-line react-hooks/exhaustive-deps
44
+ }, [dismissOverlay, dialogProps?.onDismiss, modalProps?.onDismiss]);
41
45
  const toggleExpandable = useCallback(() => visible ? closeExpandable() : openExpandable(), [visible, openExpandable, closeExpandable]);
42
46
  useImperativeHandle(ref, () => ({
43
47
  openExpandable,
@@ -45,7 +49,7 @@ const ExpandableOverlay = (props, ref) => {
45
49
  toggleExpandable
46
50
  }));
47
51
  const renderModal = () => {
48
- return <Modal testID={`${testID}.overlay`} overlayBackgroundColor={Colors.$backgroundDefault} {...modalProps} visible={visible} onDismiss={closeExpandable} onRequestClose={closeExpandable} onBackgroundPress={closeExpandable}>
52
+ return <Modal testID={`${testID}.overlay`} overlayBackgroundColor={Colors.$backgroundDefault} {...modalProps} visible={visible} onDismiss={dismissOverlay} onRequestClose={closeExpandable} onBackgroundPress={closeExpandable}>
49
53
  {showTopBar && <Modal.TopBar onDone={closeExpandable} {...topBarProps} />}
50
54
  {expandableContent}
51
55
  </Modal>;
package/src/index.d.ts CHANGED
@@ -38,6 +38,7 @@ export { default as ExpandableSection, ExpandableSectionProps } from './componen
38
38
  export { default as Fader, FaderProps, FaderPosition } from './components/fader';
39
39
  export { default as FeatureHighlight, FeatureHighlightProps } from './components/featureHighlight';
40
40
  export { default as FloatingButton, FloatingButtonProps, FloatingButtonLayouts } from './components/floatingButton';
41
+ export { default as ScreenFooter, ScreenFooterProps, ScreenFooterLayouts, ScreenFooterBackgrounds, FooterAlignment, HorizontalItemsDistribution, ItemsFit, KeyboardBehavior, ScreenFooterShadow } from './components/screenFooter';
41
42
  export { default as Gradient, GradientProps, GradientTypes } from './components/gradient';
42
43
  export { default as Slider } from './components/slider';
43
44
  export { default as GradientSlider } from './components/slider/GradientSlider';
package/src/index.js CHANGED
@@ -99,6 +99,15 @@ var _exportNames = {
99
99
  FloatingButton: true,
100
100
  FloatingButtonProps: true,
101
101
  FloatingButtonLayouts: true,
102
+ ScreenFooter: true,
103
+ ScreenFooterProps: true,
104
+ ScreenFooterLayouts: true,
105
+ ScreenFooterBackgrounds: true,
106
+ FooterAlignment: true,
107
+ HorizontalItemsDistribution: true,
108
+ ItemsFit: true,
109
+ KeyboardBehavior: true,
110
+ ScreenFooterShadow: true,
102
111
  Gradient: true,
103
112
  GradientProps: true,
104
113
  GradientTypes: true,
@@ -699,6 +708,12 @@ Object.defineProperty(exports, "FloatingButtonProps", {
699
708
  return _floatingButton().FloatingButtonProps;
700
709
  }
701
710
  });
711
+ Object.defineProperty(exports, "FooterAlignment", {
712
+ enumerable: true,
713
+ get: function () {
714
+ return _screenFooter().FooterAlignment;
715
+ }
716
+ });
702
717
  Object.defineProperty(exports, "ForwardRefInjectedProps", {
703
718
  enumerable: true,
704
719
  get: function () {
@@ -790,6 +805,12 @@ Object.defineProperty(exports, "HintProps", {
790
805
  }
791
806
  });
792
807
  exports.Hooks = void 0;
808
+ Object.defineProperty(exports, "HorizontalItemsDistribution", {
809
+ enumerable: true,
810
+ get: function () {
811
+ return _screenFooter().HorizontalItemsDistribution;
812
+ }
813
+ });
793
814
  Object.defineProperty(exports, "Icon", {
794
815
  enumerable: true,
795
816
  get: function () {
@@ -815,6 +836,12 @@ Object.defineProperty(exports, "ImageProps", {
815
836
  }
816
837
  });
817
838
  exports.Incubator = void 0;
839
+ Object.defineProperty(exports, "ItemsFit", {
840
+ enumerable: true,
841
+ get: function () {
842
+ return _screenFooter().ItemsFit;
843
+ }
844
+ });
818
845
  Object.defineProperty(exports, "KeyboardAwareFlatList", {
819
846
  enumerable: true,
820
847
  get: function () {
@@ -827,6 +854,12 @@ Object.defineProperty(exports, "KeyboardAwareScrollView", {
827
854
  return _KeyboardAwareScrollView().default;
828
855
  }
829
856
  });
857
+ Object.defineProperty(exports, "KeyboardBehavior", {
858
+ enumerable: true,
859
+ get: function () {
860
+ return _screenFooter().KeyboardBehavior;
861
+ }
862
+ });
830
863
  Object.defineProperty(exports, "ListItem", {
831
864
  enumerable: true,
832
865
  get: function () {
@@ -1146,6 +1179,36 @@ Object.defineProperty(exports, "RenderCustomModalProps", {
1146
1179
  return _picker().RenderCustomModalProps;
1147
1180
  }
1148
1181
  });
1182
+ Object.defineProperty(exports, "ScreenFooter", {
1183
+ enumerable: true,
1184
+ get: function () {
1185
+ return _screenFooter().default;
1186
+ }
1187
+ });
1188
+ Object.defineProperty(exports, "ScreenFooterBackgrounds", {
1189
+ enumerable: true,
1190
+ get: function () {
1191
+ return _screenFooter().ScreenFooterBackgrounds;
1192
+ }
1193
+ });
1194
+ Object.defineProperty(exports, "ScreenFooterLayouts", {
1195
+ enumerable: true,
1196
+ get: function () {
1197
+ return _screenFooter().ScreenFooterLayouts;
1198
+ }
1199
+ });
1200
+ Object.defineProperty(exports, "ScreenFooterProps", {
1201
+ enumerable: true,
1202
+ get: function () {
1203
+ return _screenFooter().ScreenFooterProps;
1204
+ }
1205
+ });
1206
+ Object.defineProperty(exports, "ScreenFooterShadow", {
1207
+ enumerable: true,
1208
+ get: function () {
1209
+ return _screenFooter().ScreenFooterShadow;
1210
+ }
1211
+ });
1149
1212
  Object.defineProperty(exports, "ScrollBar", {
1150
1213
  enumerable: true,
1151
1214
  get: function () {
@@ -1906,6 +1969,13 @@ function _floatingButton() {
1906
1969
  };
1907
1970
  return data;
1908
1971
  }
1972
+ function _screenFooter() {
1973
+ const data = _interopRequireWildcard(require("./components/screenFooter"));
1974
+ _screenFooter = function () {
1975
+ return data;
1976
+ };
1977
+ return data;
1978
+ }
1909
1979
  function _gradient() {
1910
1980
  const data = _interopRequireWildcard(require("./components/gradient"));
1911
1981
  _gradient = function () {
@@ -19,9 +19,15 @@ export type GetColorByHexOptions = {
19
19
  export type GeneratePaletteOptions = {
20
20
  /** Whether to adjust the lightness of very light colors (generating darker palette) */
21
21
  adjustLightness?: boolean;
22
- /** Whether to adjust the saturation of colors with high lightness and saturation (unifying saturation level throughout palette) */
22
+ /** Whether to apply the saturation curve to unify saturation levels throughout the palette */
23
23
  adjustSaturation?: boolean;
24
- /** Array of saturation adjustments to apply on the color's tints array (from darkest to lightest).
24
+ /** Custom percentage-based saturation curve indexed by distance from the base color.
25
+ * Overrides the default curve when provided. Each value represents the fraction of the base
26
+ * color's saturation to apply at that distance (e.g. [1.0, 0.89, 0.77, ...]).
27
+ * The 'adjustSaturation' option must be true */
28
+ saturationCurve?: number[];
29
+ /** Array of additive saturation adjustments to apply per-index on the palette (from darkest to lightest).
30
+ * When provided, uses legacy per-index saturation logic instead of the default curve.
25
31
  * The 'adjustSaturation' option must be true */
26
32
  saturationLevels?: number[];
27
33
  /** Whether to add two extra dark colors usually used for dark mode (generating a palette of 10 instead of 8 colors) */
@@ -91,7 +97,6 @@ export declare class Colors {
91
97
  adjustSaturation: boolean;
92
98
  addDarkestTints: boolean;
93
99
  avoidReverseOnDark: boolean;
94
- saturationLevels: undefined;
95
100
  };
96
101
  generateColorPalette: ((color: string, options?: GeneratePaletteOptions) => string[]) & _.MemoizedFunction;
97
102
  private generateDesignTokens;
@@ -144,7 +149,7 @@ declare const colorObject: Colors & {
144
149
  green70: string;
145
150
  green80: string;
146
151
  yellow1: string;
147
- yellow5: string; /** Whether to adjust the saturation of colors with high lightness and saturation (unifying saturation level throughout palette) */
152
+ yellow5: string;
148
153
  yellow10: string;
149
154
  yellow20: string;
150
155
  yellow30: string;
@@ -212,7 +217,7 @@ declare const colorObject: Colors & {
212
217
  $backgroundGeneralHeavy: string;
213
218
  $backgroundGeneralMedium: string;
214
219
  $backgroundGeneralLight: string;
215
- $backgroundSuccessHeavy: string; /** Whether to adjust the lightness of very light colors (generating darker palette) */
220
+ $backgroundSuccessHeavy: string;
216
221
  $backgroundSuccessLight: string;
217
222
  $backgroundWarningHeavy: string;
218
223
  $backgroundWarningLight: string;
@@ -258,16 +263,10 @@ declare const colorObject: Colors & {
258
263
  $outlineNeutralHeavy: string;
259
264
  $outlinePrimary: string;
260
265
  $outlinePrimaryMedium: string;
261
- $outlineGeneral: string; /**
262
- * Get app's current color scheme
263
- */
266
+ $outlineGeneral: string;
264
267
  $outlineWarning: string;
265
268
  $outlineDanger: string;
266
- $outlineInverted: string; /**
267
- * Set color scheme for app
268
- * arguments:
269
- * scheme - color scheme e.g light/dark/default
270
- */
269
+ $outlineInverted: string;
271
270
  $black: string;
272
271
  $white: string;
273
272
  };
@@ -1,4 +1,3 @@
1
- import _map from "lodash/map";
2
1
  import _clamp from "lodash/clamp";
3
2
  import _toLower from "lodash/toLower";
4
3
  import _toUpper from "lodash/toUpper";
@@ -20,6 +19,9 @@ import DesignTokensDM from "./designTokensDM";
20
19
  import ColorName from "./colorName";
21
20
  import Scheme from "./scheme";
22
21
  import LogService from "../services/LogService";
22
+ const SATURATION_CURVE = [1.0, 0.89, 0.77, 0.65, 0.55, 0.47, 0.42, 0.38, 0.34, 0.30];
23
+ const SATURATION_THRESHOLD = 50;
24
+ const SATURATION_FLOOR = 20;
23
25
  export class Colors {
24
26
  shouldSupportDarkMode = false;
25
27
  constructor() {
@@ -229,15 +231,14 @@ export class Colors {
229
231
  const start = options?.addDarkestTints && colorLightness > 10 ? -size : 0;
230
232
  const end = options?.addDarkestTints && colorLightness > 10 ? undefined : size;
231
233
  const sliced = tints.slice(start, end);
232
- const adjusted = options?.adjustSaturation && adjustSaturation(sliced, color, options?.saturationLevels);
234
+ const adjusted = options?.adjustSaturation && adjustSaturation(sliced, color, options);
233
235
  return adjusted || sliced;
234
236
  }, generatePaletteCacheResolver);
235
237
  defaultPaletteOptions = {
236
238
  adjustLightness: true,
237
239
  adjustSaturation: true,
238
240
  addDarkestTints: false,
239
- avoidReverseOnDark: false,
240
- saturationLevels: undefined
241
+ avoidReverseOnDark: false
241
242
  };
242
243
  generateColorPalette = _memoize((color, options) => {
243
244
  const _options = {
@@ -308,46 +309,46 @@ export class Colors {
308
309
  function colorStringValue(color) {
309
310
  return color?.toString();
310
311
  }
311
- function adjustAllSaturations(colors, baseColor, levels) {
312
- const array = [];
313
- _forEach(colors, (c, index) => {
314
- if (c === baseColor) {
315
- array[index] = baseColor;
316
- } else {
317
- const hsl = Color(c).hsl();
318
- const saturation = hsl.color[1];
319
- const level = levels[index];
320
- if (level !== undefined) {
321
- const saturationLevel = saturation + level;
322
- const clampedLevel = _clamp(saturationLevel, 0, 100);
323
- const adjusted = setSaturation(c, clampedLevel);
324
- array[index] = adjusted;
325
- }
312
+ function adjustSaturation(colors, baseColor, options) {
313
+ if (options?.saturationLevels) {
314
+ return adjustSaturationByLevels(colors, baseColor, options.saturationLevels);
315
+ }
316
+ return adjustSaturationWithCurve(colors, baseColor, options?.saturationCurve);
317
+ }
318
+ function adjustSaturationByLevels(colors, baseColor, levels) {
319
+ return colors.map((color, index) => {
320
+ if (color === baseColor) {
321
+ return baseColor;
326
322
  }
323
+ const level = levels[index];
324
+ if (level === undefined) {
325
+ return color;
326
+ }
327
+ const hsl = Color(color).hsl();
328
+ const newSaturation = _clamp(hsl.color[1] + level, 0, 100);
329
+ return Color.hsl(hsl.color[0], newSaturation, hsl.color[2]).hex();
327
330
  });
328
- return array;
329
331
  }
330
- function adjustSaturation(colors, baseColor, levels) {
331
- if (levels) {
332
- return adjustAllSaturations(colors, baseColor, levels);
332
+ function adjustSaturationWithCurve(colors, baseColor, customCurve) {
333
+ const baseSaturation = Color(baseColor).hsl().color[1];
334
+ if (baseSaturation <= SATURATION_THRESHOLD) {
335
+ return null;
333
336
  }
334
- let array;
335
- const lightnessLevel = 80;
336
- const saturationLevel = 60;
337
- const hsl = Color(baseColor).hsl();
338
- const lightness = Math.round(hsl.color[2]);
339
- if (lightness > lightnessLevel) {
340
- const saturation = Math.round(hsl.color[1]);
341
- if (saturation > saturationLevel) {
342
- array = _map(colors, e => e !== baseColor ? setSaturation(e, saturationLevel) : e);
343
- }
337
+ const baseIndex = colors.indexOf(baseColor.toUpperCase());
338
+ if (baseIndex === -1) {
339
+ return null;
344
340
  }
345
- return array;
346
- }
347
- function setSaturation(color, saturation) {
348
- const hsl = Color(color).hsl();
349
- hsl.color[1] = saturation;
350
- return hsl.hex();
341
+ const curve = customCurve ?? SATURATION_CURVE;
342
+ return colors.map((hex, i) => {
343
+ if (i === baseIndex) {
344
+ return hex;
345
+ }
346
+ const hsl = Color(hex).hsl();
347
+ const distance = Math.abs(i - baseIndex);
348
+ const percentage = curve[Math.min(distance, curve.length - 1)];
349
+ const newSaturation = Math.max(SATURATION_FLOOR, Math.round(baseSaturation * percentage));
350
+ return Color.hsl(hsl.color[0], newSaturation, hsl.color[2]).hex();
351
+ });
351
352
  }
352
353
  function generateColorTint(color, tintLevel) {
353
354
  const hsl = Color(color).hsl();