@idealyst/components 1.2.119 → 1.2.122

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/components",
3
- "version": "1.2.119",
3
+ "version": "1.2.122",
4
4
  "description": "Shared component library for React and React Native",
5
5
  "documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/components#readme",
6
6
  "readme": "README.md",
@@ -56,12 +56,13 @@
56
56
  "publish:npm": "npm publish"
57
57
  },
58
58
  "peerDependencies": {
59
- "@idealyst/theme": "^1.2.119",
59
+ "@idealyst/theme": "^1.2.122",
60
60
  "@mdi/js": ">=7.0.0",
61
61
  "@mdi/react": ">=1.0.0",
62
62
  "@react-native-vector-icons/common": ">=12.0.0",
63
63
  "@react-native-vector-icons/material-design-icons": ">=12.0.0",
64
64
  "@react-native/normalize-colors": "*",
65
+ "@react-navigation/bottom-tabs": ">=7.0.0",
65
66
  "react": ">=16.8.0",
66
67
  "react-native": ">=0.60.0",
67
68
  "react-native-edge-to-edge": "*",
@@ -80,6 +81,9 @@
80
81
  "@mdi/react": {
81
82
  "optional": true
82
83
  },
84
+ "@react-navigation/bottom-tabs": {
85
+ "optional": true
86
+ },
83
87
  "@react-native/normalize-colors": {
84
88
  "optional": true
85
89
  },
@@ -107,8 +111,8 @@
107
111
  },
108
112
  "devDependencies": {
109
113
  "@idealyst/blur": "^1.2.40",
110
- "@idealyst/theme": "^1.2.119",
111
- "@idealyst/tooling": "^1.2.119",
114
+ "@idealyst/theme": "^1.2.122",
115
+ "@idealyst/tooling": "^1.2.122",
112
116
  "@mdi/react": "^1.6.1",
113
117
  "@types/react": "^19.1.0",
114
118
  "react": "^19.1.0",
@@ -18,6 +18,7 @@ const Dialog = forwardRef<View, DialogProps>(({
18
18
  animationType: _animationType = 'fade',
19
19
  avoidKeyboard = false,
20
20
  padding: paddingProp = 20,
21
+ height,
21
22
  maxContentHeight,
22
23
  contentPadding = 24,
23
24
  contentStyle,
@@ -183,14 +184,30 @@ const Dialog = forwardRef<View, DialogProps>(({
183
184
  ? Math.min(maxContentHeight, maxAvailableHeight)
184
185
  : maxAvailableHeight;
185
186
 
186
- // Dialog uses the effective max height, with flex: 1 so children can fill it
187
+ // Resolve explicit height (number or percentage string)
188
+ const resolvedHeight = typeof height === 'string'
189
+ ? height.endsWith('%')
190
+ ? (parseFloat(height) / 100) * maxAvailableHeight
191
+ : parseFloat(height)
192
+ : height;
193
+
194
+ // Dialog uses the effective max height, with a definite height when requested
195
+ // so children can resolve flex: 1 against it
187
196
  const dialogContainerStyle = {
188
197
  ...containerStyle,
189
198
  maxHeight: effectiveMaxHeight,
190
- height: maxContentHeight ? effectiveMaxHeight : undefined,
199
+ height: resolvedHeight
200
+ ? Math.min(resolvedHeight, effectiveMaxHeight)
201
+ : maxContentHeight
202
+ ? effectiveMaxHeight
203
+ : undefined,
191
204
  flex: undefined,
192
205
  };
193
206
 
207
+ // Only apply flex: 1 to content when the dialog has a definite height to flex against.
208
+ // Without a definite height, flex: 1 collapses content instead of wrapping naturally.
209
+ const hasDefiniteHeight = Boolean(resolvedHeight || maxContentHeight);
210
+
194
211
  const dialogContainer = (
195
212
  <Animated.View ref={ref as any} style={[dialogContainerStyle, style, containerAnimatedStyle]} nativeID={id} {...nativeA11yProps}>
196
213
  {(title || showCloseButton) && (
@@ -212,7 +229,7 @@ const Dialog = forwardRef<View, DialogProps>(({
212
229
  )}
213
230
  </View>
214
231
  )}
215
- <View style={[contentPadding > 0 ? { padding: contentPadding } : undefined, contentStyle]}>
232
+ <View style={[hasDefiniteHeight && { flex: 1, minHeight: 0 }, contentPadding > 0 ? { padding: contentPadding } : undefined, contentStyle]}>
216
233
  {children}
217
234
  </View>
218
235
  </Animated.View>
@@ -5,6 +5,7 @@ import {
5
5
  Pressable,
6
6
  ScrollView,
7
7
  } from 'react-native';
8
+ import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated';
8
9
  import { menuStyles } from './Menu.styles';
9
10
  import type { MenuProps, MenuItem as MenuItemType } from './types';
10
11
  import MenuItem from './MenuItem.native';
@@ -60,10 +61,33 @@ const Menu = forwardRef<IdealystElement, MenuProps>(({
60
61
 
61
62
  const mergedTriggerRef = useMergeRefs(ref, triggerRef);
62
63
 
64
+ // Animation shared values
65
+ const menuOpacity = useSharedValue(0);
66
+
67
+ const animatedMenuStyle = useAnimatedStyle(() => ({
68
+ opacity: menuOpacity.value,
69
+ }));
70
+
71
+ // Animate in when measured and positioned
72
+ const isMeasured = menuSize.height > 0;
73
+ const shouldShow = isMeasured && isPositioned;
74
+
75
+ useEffect(() => {
76
+ if (shouldShow) {
77
+ menuOpacity.value = withTiming(1, {
78
+ duration: 150,
79
+ easing: Easing.out(Easing.ease),
80
+ });
81
+ } else {
82
+ menuOpacity.value = 0;
83
+ }
84
+ }, [shouldShow]);
85
+
63
86
  // Reset position when menu closes
64
87
  useEffect(() => {
65
88
  if (!open) {
66
89
  resetPosition();
90
+ menuOpacity.value = 0;
67
91
  }
68
92
  }, [open]);
69
93
 
@@ -93,10 +117,6 @@ const Menu = forwardRef<IdealystElement, MenuProps>(({
93
117
  const renderMenu = () => {
94
118
  if (!open) return null;
95
119
 
96
- // Show menu only after it has been measured AND positioned
97
- const isMeasured = menuSize.height > 0;
98
- const shouldShow = isMeasured && isPositioned;
99
-
100
120
  return (
101
121
  <Modal
102
122
  visible={true}
@@ -115,26 +135,27 @@ const Menu = forwardRef<IdealystElement, MenuProps>(({
115
135
  style={[
116
136
  (menuStyles.menu as any)({}),
117
137
  style,
118
- { opacity: shouldShow ? 1 : 0 }
119
138
  ]}
120
139
  onLayout={handleMenuLayout}
121
140
  >
122
- <ScrollView
123
- showsVerticalScrollIndicator={false}
124
- >
125
- {items.map((item, index) => {
126
-
127
- return (
128
- <MenuItem
129
- key={item.id || index}
130
- item={item}
131
- onPress={handleItemPress}
132
- size={size}
133
- testID={testID ? `${testID}-item-${item.id || index}` : undefined}
134
- />
135
- );
136
- })}
137
- </ScrollView>
141
+ <Animated.View style={animatedMenuStyle}>
142
+ <ScrollView
143
+ showsVerticalScrollIndicator={false}
144
+ >
145
+ {items.map((item, index) => {
146
+
147
+ return (
148
+ <MenuItem
149
+ key={item.id || index}
150
+ item={item}
151
+ onPress={handleItemPress}
152
+ size={size}
153
+ testID={testID ? `${testID}-item-${item.id || index}` : undefined}
154
+ />
155
+ );
156
+ })}
157
+ </ScrollView>
158
+ </Animated.View>
138
159
  </BoundedModalContent>
139
160
  </Pressable>
140
161
  </Modal>
@@ -24,19 +24,28 @@ const MenuItem = forwardRef<IdealystElement, MenuItemProps>(({ item, onPress, si
24
24
  const iconStyle = (menuItemStyles.icon as any)({});
25
25
  const labelStyle = (menuItemStyles.label as any)({});
26
26
 
27
+ // Extract icon size from theme variant (fontSize is set by $menu.iconSize)
28
+ const iconSize = iconStyle.fontSize || iconStyle.width || 20;
29
+
27
30
  const renderIcon = () => {
28
31
  if (!item.icon) return null;
29
32
 
30
33
  if (typeof item.icon === 'string') {
31
34
  return (
32
- <MaterialDesignIcons
33
- name={item.icon as any}
34
- color={iconStyle.color}
35
- style={iconStyle}
36
- />
35
+ <View style={{ width: iconSize, height: iconSize, alignItems: 'center', justifyContent: 'center', marginRight: iconStyle.marginRight || 12, flexShrink: 0 }}>
36
+ <MaterialDesignIcons
37
+ name={item.icon as any}
38
+ size={iconSize}
39
+ color={iconStyle.color}
40
+ />
41
+ </View>
37
42
  );
38
43
  } else if (isValidElement(item.icon)) {
39
- return item.icon;
44
+ return (
45
+ <View style={{ marginRight: iconStyle.marginRight || 12, flexShrink: 0 }}>
46
+ {item.icon}
47
+ </View>
48
+ );
40
49
  }
41
50
  return null;
42
51
  };
@@ -54,12 +63,8 @@ const MenuItem = forwardRef<IdealystElement, MenuItemProps>(({ item, onPress, si
54
63
  android_ripple={{ color: 'rgba(0, 0, 0, 0.1)' }}
55
64
  testID={testID}
56
65
  >
57
- {item.icon && (
58
- <View>
59
- {renderIcon()}
60
- </View>
61
- )}
62
- <Text style={labelStyle}>
66
+ {renderIcon()}
67
+ <Text style={labelStyle} numberOfLines={1}>
63
68
  {item.label}
64
69
  </Text>
65
70
  </Pressable>
@@ -1,7 +1,8 @@
1
- import { forwardRef, useEffect } from 'react';
1
+ import { forwardRef, useContext, useEffect } from 'react';
2
2
  import { View as RNView, ScrollView as RNScrollView, Keyboard, Platform } from 'react-native';
3
3
  import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated';
4
4
  import { useSafeAreaInsets } from '@idealyst/theme';
5
+ import { BottomTabBarHeightContext } from '@react-navigation/bottom-tabs';
5
6
  import { ScreenProps } from './types';
6
7
  import { screenStyles } from './Screen.styles';
7
8
  import type { IdealystElement } from '../utils/refTypes';
@@ -16,6 +17,7 @@ const Screen = forwardRef<IdealystElement, ScreenProps>(({
16
17
  safeAreaRight,
17
18
  scrollable = true,
18
19
  avoidKeyboard = true,
20
+ keyboardAvoidingOffset = 0,
19
21
  contentInset,
20
22
  onLayout,
21
23
  // Spacing variants from ContainerStyleProps
@@ -38,6 +40,9 @@ const Screen = forwardRef<IdealystElement, ScreenProps>(({
38
40
  const applySafeAreaLeft = safeAreaLeft ?? safeArea;
39
41
  const applySafeAreaRight = safeAreaRight ?? safeArea;
40
42
 
43
+ // Detect tab bar height when inside a bottom tab navigator
44
+ const tabBarHeight = useContext(BottomTabBarHeightContext) ?? 0;
45
+
41
46
  // Animated keyboard offset
42
47
  const keyboardOffset = useSharedValue(0);
43
48
 
@@ -49,7 +54,10 @@ const Screen = forwardRef<IdealystElement, ScreenProps>(({
49
54
  const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
50
55
 
51
56
  const showSubscription = Keyboard.addListener(showEvent, (e) => {
52
- keyboardOffset.value = withTiming(e.endCoordinates.height, {
57
+ // Subtract the tab bar height and any manual offset since those areas
58
+ // are already occupied and don't need additional padding
59
+ const adjustedHeight = Math.max(0, e.endCoordinates.height - tabBarHeight - keyboardAvoidingOffset);
60
+ keyboardOffset.value = withTiming(adjustedHeight, {
53
61
  duration: Platform.OS === 'ios' ? e.duration : 250,
54
62
  easing: Easing.out(Easing.cubic),
55
63
  });
@@ -66,7 +74,7 @@ const Screen = forwardRef<IdealystElement, ScreenProps>(({
66
74
  showSubscription.remove();
67
75
  hideSubscription.remove();
68
76
  };
69
- }, [avoidKeyboard]);
77
+ }, [avoidKeyboard, tabBarHeight, keyboardAvoidingOffset]);
70
78
 
71
79
  const animatedKeyboardStyle = useAnimatedStyle(() => ({
72
80
  paddingBottom: keyboardOffset.value,
@@ -84,6 +84,15 @@ export interface ScreenProps extends ContainerStyleProps {
84
84
  */
85
85
  avoidKeyboard?: boolean;
86
86
 
87
+ /**
88
+ * Additional offset to subtract from the keyboard height when avoiding the keyboard.
89
+ * The Screen automatically accounts for the tab bar height when inside a tab navigator,
90
+ * but this prop can be used for other fixed UI elements that occupy space at the bottom.
91
+ * @default 0
92
+ * @platform native
93
+ */
94
+ keyboardAvoidingOffset?: number;
95
+
87
96
  /**
88
97
  * Called when the layout of the screen changes.
89
98
  * Provides the new width, height, x, and y coordinates.
@@ -17,6 +17,7 @@ const TextArea = forwardRef<IdealystElement, TextAreaProps>(({
17
17
  minHeight,
18
18
  maxHeight,
19
19
  autoGrow = false,
20
+ fill = false,
20
21
  maxLength,
21
22
  rows = 4,
22
23
  label,
@@ -167,12 +168,12 @@ const TextArea = forwardRef<IdealystElement, TextAreaProps>(({
167
168
  });
168
169
 
169
170
  return (
170
- <View nativeID={id} style={[containerStyleComputed, style]} testID={testID}>
171
+ <View nativeID={id} style={[containerStyleComputed, fill && { flex: 1 }, style]} testID={testID}>
171
172
  {label && (
172
173
  <Text style={labelStyleComputed}>{label}</Text>
173
174
  )}
174
175
 
175
- <View style={textareaContainerStyleComputed}>
176
+ <View style={[textareaContainerStyleComputed, fill && { flex: 1 }]}>
176
177
  <TextInput
177
178
  ref={useMergeRefs(textInputRef, ref as any)}
178
179
  {...nativeA11yProps}
@@ -183,16 +184,19 @@ const TextArea = forwardRef<IdealystElement, TextAreaProps>(({
183
184
  textAlignVertical: autoGrow ? 'center' : 'top',
184
185
  backgroundColor: 'transparent',
185
186
  },
186
- // For autoGrow: don't set height, let it grow naturally with minHeight constraint
187
- // For fixed height: use rows-based height
188
- autoGrow
189
- ? {
190
- minHeight: minHeight ?? 44,
191
- maxHeight: maxHeight,
192
- // Force height to minHeight when empty to ensure shrinking
193
- ...(value === '' ? { height: minHeight ?? 44 } : {}),
194
- }
195
- : { height: rows * 24 },
187
+ // fill: expand to fill available space via flex
188
+ // autoGrow: don't set height, let it grow naturally with minHeight constraint
189
+ // default: use rows-based height
190
+ fill
191
+ ? { flex: 1 }
192
+ : autoGrow
193
+ ? {
194
+ minHeight: minHeight ?? 44,
195
+ maxHeight: maxHeight,
196
+ // Force height to minHeight when empty to ensure shrinking
197
+ ...(value === '' ? { height: minHeight ?? 44 } : {}),
198
+ }
199
+ : { height: rows * 24 },
196
200
  textareaStyle,
197
201
  ]}
198
202
  value={value}
@@ -21,6 +21,7 @@ const TextArea = forwardRef<IdealystElement, TextAreaProps>(({
21
21
  minHeight,
22
22
  maxHeight,
23
23
  autoGrow = false,
24
+ fill = false,
24
25
  maxLength,
25
26
  label,
26
27
  error,
@@ -158,7 +159,7 @@ const TextArea = forwardRef<IdealystElement, TextAreaProps>(({
158
159
  const characterCountProps = getWebProps([characterCountStyleComputed]);
159
160
 
160
161
  const adjustHeight = useCallback(() => {
161
- if (!autoGrow || !textareaRef.current) return;
162
+ if (!autoGrow || fill || !textareaRef.current) return;
162
163
 
163
164
  const textarea = textareaRef.current;
164
165
 
@@ -176,7 +177,7 @@ const TextArea = forwardRef<IdealystElement, TextAreaProps>(({
176
177
  }
177
178
 
178
179
  textarea.style.height = `${newHeight}px`;
179
- }, [autoGrow, minHeight, maxHeight]);
180
+ }, [autoGrow, fill, minHeight, maxHeight]);
180
181
 
181
182
  useEffect(() => {
182
183
  adjustHeight();
@@ -243,17 +244,18 @@ const TextArea = forwardRef<IdealystElement, TextAreaProps>(({
243
244
  const mergedTextareaRef = useMergeRefs(textareaRef, computedTextareaProps.ref);
244
245
 
245
246
  return (
246
- <div {...containerProps} ref={mergedRef} id={id} data-testid={testID}>
247
+ <div {...containerProps} ref={mergedRef} id={id} data-testid={testID} style={{ ...containerProps.style as any, ...(fill ? { flex: 1, display: 'flex', flexDirection: 'column' } : {}) }}>
247
248
  {label && (
248
249
  <label {...labelProps} id={labelId} htmlFor={textareaId}>{label}</label>
249
250
  )}
250
251
 
251
- <div {...textareaContainerProps}>
252
+ <div {...textareaContainerProps} style={{ ...textareaContainerProps.style as any, ...(fill ? { flex: 1, display: 'flex', flexDirection: 'column' } : {}) }}>
252
253
  <textarea
253
254
  {...computedTextareaProps}
254
255
  {...ariaProps}
255
256
  id={textareaId}
256
257
  ref={mergedTextareaRef}
258
+ style={{ ...computedTextareaProps.style as any, ...(fill ? { flex: 1 } : {}) }}
257
259
  value={value}
258
260
  onChange={handleChange}
259
261
  onFocus={handleFocus}
@@ -261,7 +263,7 @@ const TextArea = forwardRef<IdealystElement, TextAreaProps>(({
261
263
  onKeyDown={handleKeyDown}
262
264
  placeholder={placeholder}
263
265
  disabled={disabled}
264
- rows={autoGrow ? undefined : rows}
266
+ rows={fill ? undefined : autoGrow ? undefined : rows}
265
267
  maxLength={maxLength}
266
268
  />
267
269
  </div>
@@ -44,6 +44,11 @@ export interface TextAreaProps extends FormInputStyleProps, FormAccessibilityPro
44
44
  minHeight?: number;
45
45
  maxHeight?: number;
46
46
  autoGrow?: boolean;
47
+ /**
48
+ * When true, the textarea expands to fill available vertical space using flex.
49
+ * Overrides `rows` and `autoGrow` height logic. All container layers get `flex: 1`.
50
+ */
51
+ fill?: boolean;
47
52
  maxLength?: number;
48
53
  label?: string;
49
54
  error?: string;
@@ -93,4 +93,5 @@ export type {
93
93
  ImageStyleableElements,
94
94
  PressableStyleableElements,
95
95
  ScreenStyleableElements,
96
+ DatePickerCalendarStyleableElements,
96
97
  } from './types';
@@ -426,6 +426,37 @@ export type ScreenStyleableElements = {
426
426
  content: Styles;
427
427
  };
428
428
 
429
+ /**
430
+ * DatePickerCalendar styleable elements.
431
+ * @see datePickerCalendarStyles in @idealyst/datepicker
432
+ */
433
+ export type DatePickerCalendarStyleableElements = {
434
+ calendar: ElementStyle;
435
+ calendarHeader: ElementStyle;
436
+ calendarTitle: ElementStyle;
437
+ weekdayRow: ElementStyle;
438
+ weekdayCell: ElementStyle;
439
+ weekdayText: ElementStyle;
440
+ calendarGrid: ElementStyle;
441
+ dayCell: ElementStyle;
442
+ dayButton: ElementStyle;
443
+ dayText: ElementStyle;
444
+ selectedDay: ElementStyle;
445
+ selectedDayText: ElementStyle;
446
+ todayDay: ElementStyle;
447
+ navButton: ElementStyle;
448
+ titleButton: ElementStyle;
449
+ titleText: ElementStyle;
450
+ monthGrid: ElementStyle;
451
+ yearGrid: ElementStyle;
452
+ selectorItem: ElementStyle;
453
+ selectorItemSelected: ElementStyle;
454
+ selectorItemText: ElementStyle;
455
+ selectorItemTextSelected: ElementStyle;
456
+ indicatorRow: ElementStyle;
457
+ indicator: ElementStyle;
458
+ };
459
+
429
460
  // ============================================================================
430
461
  // Master Component Style Elements Map
431
462
  // ============================================================================
@@ -502,6 +533,7 @@ interface BuiltInComponentStyleElements {
502
533
  Image: ImageStyleableElements;
503
534
  Pressable: PressableStyleableElements;
504
535
  Screen: ScreenStyleableElements;
536
+ DatePickerCalendar: DatePickerCalendarStyleableElements;
505
537
  }
506
538
 
507
539
  /**
@@ -14,7 +14,7 @@ interface BoundedModalContentProps {
14
14
 
15
15
  /**
16
16
  * A wrapper component for modal content that automatically constrains its height
17
- * to fit within screen boundaries, accounting for safe areas.
17
+ * and width to fit within screen boundaries, accounting for safe areas.
18
18
  */
19
19
  export const BoundedModalContent: React.FC<BoundedModalContentProps> = ({
20
20
  children,
@@ -26,7 +26,7 @@ export const BoundedModalContent: React.FC<BoundedModalContentProps> = ({
26
26
  onLayout,
27
27
  }) => {
28
28
  const insets = useSafeAreaInsets();
29
- const { height: windowHeight } = Dimensions.get('window');
29
+ const { width: windowWidth, height: windowHeight } = Dimensions.get('window');
30
30
  const padding = 12;
31
31
 
32
32
  // Calculate dynamic maxHeight to ensure content stays within bounds
@@ -38,6 +38,9 @@ export const BoundedModalContent: React.FC<BoundedModalContentProps> = ({
38
38
  // Calculate available height: from current top position to bottom boundary
39
39
  const availableHeight = Math.max(100, bottomBound - top);
40
40
 
41
+ // Calculate max width so content doesn't overflow the right edge
42
+ const maxAvailableWidth = windowWidth - left - insets.right - padding;
43
+
41
44
  return (
42
45
  <View
43
46
  style={[
@@ -45,7 +48,7 @@ export const BoundedModalContent: React.FC<BoundedModalContentProps> = ({
45
48
  position: 'absolute',
46
49
  top,
47
50
  left,
48
- ...(width && { width }),
51
+ ...(width ? { width } : { maxWidth: maxAvailableWidth }),
49
52
  maxHeight: availableHeight,
50
53
  },
51
54
  style,