@idealyst/components 1.2.6 → 1.2.8

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,5 +1,6 @@
1
1
  import React, { isValidElement, useState, useMemo, useRef } from 'react';
2
2
  import { getWebProps } from 'react-native-unistyles/web';
3
+ import { useUnistyles } from 'react-native-unistyles';
3
4
  import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
4
5
  import { isIconName } from '../Icon/icon-resolver';
5
6
  import useMergeRefs from '../hooks/useMergeRefs';
@@ -59,6 +60,11 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
59
60
  const isPasswordField = inputType === 'password' || secureTextEntry;
60
61
  const shouldShowPasswordToggle = isPasswordField && (showPasswordToggle !== false);
61
62
 
63
+ // Get theme for icon sizes and colors
64
+ const { theme } = useUnistyles();
65
+ const iconSize = theme.sizes.input[size].iconSize;
66
+ const iconColor = theme.colors.text.secondary;
67
+
62
68
  const [isFocused, setIsFocused] = useState(false);
63
69
 
64
70
  const getInputType = () => {
@@ -123,15 +129,12 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
123
129
  marginHorizontal,
124
130
  });
125
131
 
126
- // Get web props for all styled elements (container uses dynamic function)
132
+ // Get web props for all styled elements (all styles are dynamic functions)
127
133
  const dynamicContainerStyle = (inputStyles.container as any)({ type, focused: isFocused, hasError, disabled });
128
134
  const {ref: containerStyleRef, ...containerProps} = getWebProps([dynamicContainerStyle, style]);
129
- const leftIconContainerProps = getWebProps([inputStyles.leftIconContainer]);
130
- const rightIconContainerProps = getWebProps([inputStyles.rightIconContainer]);
131
- const leftIconProps = getWebProps([inputStyles.leftIcon]);
132
- const rightIconProps = getWebProps([inputStyles.rightIcon]);
133
- const passwordToggleProps = getWebProps([inputStyles.passwordToggle]);
134
- const passwordToggleIconProps = getWebProps([inputStyles.passwordToggleIcon]);
135
+ const leftIconContainerProps = getWebProps([(inputStyles.leftIconContainer as any)({})]);
136
+ const rightIconContainerProps = getWebProps([(inputStyles.rightIconContainer as any)({})]);
137
+ const passwordToggleProps = getWebProps([(inputStyles.passwordToggle as any)({})]);
135
138
 
136
139
  // Get input props
137
140
  const inputWebProps = getWebProps([(inputStyles.input as any)({})]);
@@ -191,12 +194,13 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
191
194
  return (
192
195
  <IconSvg
193
196
  name={leftIcon}
194
- {...leftIconProps}
197
+ size={iconSize}
198
+ color={iconColor}
195
199
  aria-label={leftIcon}
196
200
  />
197
201
  );
198
202
  } else if (isValidElement(leftIcon)) {
199
- return <span {...leftIconProps}>{leftIcon}</span>;
203
+ return <span {...leftIconContainerProps}>{leftIcon}</span>;
200
204
  }
201
205
 
202
206
  return null;
@@ -210,12 +214,13 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
210
214
  return (
211
215
  <IconSvg
212
216
  name={rightIcon}
213
- {...rightIconProps}
217
+ size={iconSize}
218
+ color={iconColor}
214
219
  aria-label={rightIcon}
215
220
  />
216
221
  );
217
222
  } else if (isValidElement(rightIcon)) {
218
- return <span {...rightIconProps}>{rightIcon}</span>;
223
+ return <span {...rightIconContainerProps}>{rightIcon}</span>;
219
224
  }
220
225
 
221
226
  return null;
@@ -227,7 +232,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
227
232
  return (
228
233
  <IconSvg
229
234
  name={iconName}
230
- {...passwordToggleIconProps}
235
+ size={iconSize}
236
+ color={iconColor}
231
237
  aria-label={iconName}
232
238
  />
233
239
  );
@@ -1,4 +1,4 @@
1
- import React, { forwardRef, useMemo } from 'react';
1
+ import React, { forwardRef, useMemo, Children, isValidElement, cloneElement } from 'react';
2
2
  import { View, ScrollView } from 'react-native';
3
3
  import { listStyles } from './List.styles';
4
4
  import type { ListProps } from './types';
@@ -59,9 +59,21 @@ const List = forwardRef<View, ListProps>(({
59
59
  style,
60
60
  ];
61
61
 
62
+ // Process children to add isLast prop to the last child
63
+ const childArray = Children.toArray(children);
64
+ const processedChildren = childArray.map((child, index) => {
65
+ if (isValidElement(child)) {
66
+ return cloneElement(child, {
67
+ ...child.props,
68
+ isLast: index === childArray.length - 1,
69
+ });
70
+ }
71
+ return child;
72
+ });
73
+
62
74
  const content = (
63
75
  <ListProvider value={{ type, size }}>
64
- {children}
76
+ {processedChildren}
65
77
  </ListProvider>
66
78
  );
67
79
 
@@ -22,6 +22,7 @@ export type ListDynamicProps = {
22
22
  selected?: boolean;
23
23
  disabled?: boolean;
24
24
  clickable?: boolean;
25
+ isLast?: boolean;
25
26
  gap?: ViewStyleSize;
26
27
  padding?: ViewStyleSize;
27
28
  paddingVertical?: ViewStyleSize;
@@ -80,7 +81,7 @@ export const listStyles = defineStyle('List', (theme: Theme) => ({
80
81
  } as const;
81
82
  },
82
83
 
83
- item: ({ type = 'default', active = false, selected = false, disabled = false, clickable = true }: ListDynamicProps) => {
84
+ item: ({ type = 'default', active = false, selected = false, disabled = false, clickable = true, isLast = false }: ListDynamicProps) => {
84
85
  const baseStyles = {
85
86
  display: 'flex' as const,
86
87
  flexDirection: 'row' as const,
@@ -90,7 +91,8 @@ export const listStyles = defineStyle('List', (theme: Theme) => ({
90
91
  opacity: disabled ? 0.5 : 1,
91
92
  };
92
93
 
93
- const dividerStyles = type === 'divided' ? {
94
+ // Don't add divider on last item
95
+ const dividerStyles = (type === 'divided' && !isLast) ? {
94
96
  borderBottomWidth: 1,
95
97
  borderBottomColor: theme.colors.border.primary,
96
98
  } : {};
@@ -117,7 +119,8 @@ export const listStyles = defineStyle('List', (theme: Theme) => ({
117
119
  cursor: disabled ? 'not-allowed' : (clickable ? 'pointer' : 'default'),
118
120
  outline: 'none',
119
121
  transition: 'background-color 0.2s ease, border-color 0.2s ease',
120
- borderBottom: type === 'divided' ? `1px solid ${theme.colors.border.primary}` : undefined,
122
+ // Don't add divider on last item
123
+ borderBottom: (type === 'divided' && !isLast) ? `1px solid ${theme.colors.border.primary}` : undefined,
121
124
  borderLeft: selected ? `3px solid ${theme.intents.primary.primary}` : undefined,
122
125
  _hover: (disabled || !clickable) ? {} : {
123
126
  backgroundColor: theme.colors.surface.secondary,
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { Children, isValidElement, cloneElement } from 'react';
2
2
  import { getWebProps } from 'react-native-unistyles/web';
3
3
  import { listStyles } from './List.styles';
4
4
  import type { ListProps } from './types';
@@ -47,6 +47,18 @@ const List: React.FC<ListProps> = ({
47
47
 
48
48
  const containerProps = getWebProps(containerStyle);
49
49
 
50
+ // Process children to add isLast prop to the last child
51
+ const childArray = Children.toArray(children);
52
+ const processedChildren = childArray.map((child, index) => {
53
+ if (isValidElement(child)) {
54
+ return cloneElement(child, {
55
+ ...child.props,
56
+ isLast: index === childArray.length - 1,
57
+ });
58
+ }
59
+ return child;
60
+ });
61
+
50
62
  return (
51
63
  <ListProvider value={{ type, size }}>
52
64
  <div
@@ -55,7 +67,7 @@ const List: React.FC<ListProps> = ({
55
67
  id={id}
56
68
  data-testid={testID}
57
69
  >
58
- {children}
70
+ {processedChildren}
59
71
  </div>
60
72
  </ListProvider>
61
73
  );
@@ -8,7 +8,7 @@ import type { ListItemProps } from './types';
8
8
  import { useListContext } from './ListContext';
9
9
  import { getNativeSelectableAccessibilityProps } from '../utils/accessibility';
10
10
 
11
- const ListItem = forwardRef<ComponentRef<typeof View> | ComponentRef<typeof Pressable>, ListItemProps>(({
11
+ const ListItem = forwardRef<ComponentRef<typeof View> | ComponentRef<typeof Pressable>, ListItemProps & { isLast?: boolean }>(({
12
12
  id,
13
13
  label,
14
14
  children,
@@ -23,6 +23,7 @@ const ListItem = forwardRef<ComponentRef<typeof View> | ComponentRef<typeof Pres
23
23
  onPress,
24
24
  style,
25
25
  testID,
26
+ isLast = false,
26
27
  // Accessibility props
27
28
  accessibilityLabel,
28
29
  accessibilityHint,
@@ -60,7 +61,7 @@ const ListItem = forwardRef<ComponentRef<typeof View> | ComponentRef<typeof Pres
60
61
  });
61
62
 
62
63
  // Get dynamic styles - call as functions to get theme-reactive styles
63
- const itemStyle = (listStyles.item as any)({ type: effectiveVariant, disabled, clickable: isClickable });
64
+ const itemStyle = (listStyles.item as any)({ type: effectiveVariant, disabled, clickable: isClickable, isLast });
64
65
  const labelStyle = (listStyles.label as any)({ disabled, selected });
65
66
  const leadingStyle = (listStyles.leading as any)({});
66
67
  const trailingStyle = (listStyles.trailing as any)({});
@@ -8,7 +8,7 @@ import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
8
8
  import { isIconName } from '../Icon/icon-resolver';
9
9
  import { useListContext } from './ListContext';
10
10
 
11
- const ListItem: React.FC<ListItemProps> = ({
11
+ const ListItem: React.FC<ListItemProps & { isLast?: boolean }> = ({
12
12
  id,
13
13
  label,
14
14
  children,
@@ -23,6 +23,7 @@ const ListItem: React.FC<ListItemProps> = ({
23
23
  onPress,
24
24
  style,
25
25
  testID,
26
+ isLast = false,
26
27
  }) => {
27
28
  const { theme } = useUnistyles() as { theme: Theme };
28
29
  const listContext = useListContext();
@@ -41,7 +42,7 @@ const ListItem: React.FC<ListItemProps> = ({
41
42
  });
42
43
 
43
44
  // Get dynamic styles - call as functions for theme reactivity
44
- const itemStyle = (listStyles.item as any)({ type: effectiveVariant, disabled, clickable: isClickable });
45
+ const itemStyle = (listStyles.item as any)({ type: effectiveVariant, disabled, clickable: isClickable, isLast });
45
46
  const labelStyle = (listStyles.label as any)({ disabled, selected });
46
47
  const leadingStyle = (listStyles.leading as any)({});
47
48
  const trailingStyle = (listStyles.trailing as any)({});
@@ -403,7 +403,117 @@ export const ButtonExamples = () => {
403
403
  </Button>
404
404
  </View>
405
405
  </View>
406
+
407
+ {/* Loading State */}
408
+ <View gap="md">
409
+ <Text typography="subtitle1">Loading State</Text>
410
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap' }}>
411
+ <Button
412
+ type="contained"
413
+ intent="primary"
414
+ loading
415
+ onPress={() => handlePress('loading-contained')}
416
+ >
417
+ Loading
418
+ </Button>
419
+ <Button
420
+ type="outlined"
421
+ intent="primary"
422
+ loading
423
+ onPress={() => handlePress('loading-outlined')}
424
+ >
425
+ Loading
426
+ </Button>
427
+ <Button
428
+ type="text"
429
+ intent="primary"
430
+ loading
431
+ onPress={() => handlePress('loading-text')}
432
+ >
433
+ Loading
434
+ </Button>
435
+ </View>
436
+ </View>
437
+
438
+ {/* Loading with Different Intents */}
439
+ <View gap="md">
440
+ <Text typography="subtitle1">Loading Intents</Text>
441
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap' }}>
442
+ <Button
443
+ type="contained"
444
+ intent="primary"
445
+ loading
446
+ onPress={() => handlePress('loading-primary')}
447
+ >
448
+ Primary
449
+ </Button>
450
+ <Button
451
+ type="contained"
452
+ intent="success"
453
+ loading
454
+ onPress={() => handlePress('loading-success')}
455
+ >
456
+ Success
457
+ </Button>
458
+ <Button
459
+ type="contained"
460
+ intent="error"
461
+ loading
462
+ onPress={() => handlePress('loading-error')}
463
+ >
464
+ Error
465
+ </Button>
466
+ <Button
467
+ type="contained"
468
+ intent="warning"
469
+ loading
470
+ onPress={() => handlePress('loading-warning')}
471
+ >
472
+ Warning
473
+ </Button>
474
+ </View>
475
+ </View>
476
+
477
+ {/* Interactive Loading Example */}
478
+ <View gap="md">
479
+ <Text typography="subtitle1">Interactive Loading</Text>
480
+ <InteractiveLoadingButton />
481
+ </View>
406
482
  </View>
407
483
  </Screen>
408
484
  );
485
+ };
486
+
487
+ // Interactive loading button component
488
+ const InteractiveLoadingButton = () => {
489
+ const [loading, setLoading] = React.useState(false);
490
+
491
+ const handlePress = async () => {
492
+ setLoading(true);
493
+ // Simulate async operation
494
+ await new Promise(resolve => setTimeout(resolve, 2000));
495
+ setLoading(false);
496
+ };
497
+
498
+ return (
499
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap' }}>
500
+ <Button
501
+ type="contained"
502
+ intent="primary"
503
+ loading={loading}
504
+ onPress={handlePress}
505
+ >
506
+ {loading ? 'Saving...' : 'Save'}
507
+ </Button>
508
+ <Button
509
+ type="outlined"
510
+ intent="success"
511
+ loading={loading}
512
+ leftIcon="check"
513
+ onPress={handlePress}
514
+ >
515
+ {loading ? 'Processing...' : 'Submit'}
516
+ </Button>
517
+ </View>
518
+ );
409
519
  };
@@ -122,6 +122,44 @@ export const IconExamples = () => {
122
122
  </View>
123
123
  </View>
124
124
 
125
+ {/* Text Colors */}
126
+ <View gap="md">
127
+ <Text typography="subtitle1">Text Colors</Text>
128
+ <View style={{ flexDirection: 'row', gap: 16, flexWrap: 'wrap', alignItems: 'center' }}>
129
+ <View style={{ alignItems: 'center', gap: 4 }}>
130
+ <Icon name="account" size="md" textColor="primary" />
131
+ <Text typography="body2">Primary</Text>
132
+ </View>
133
+ <View style={{ alignItems: 'center', gap: 4 }}>
134
+ <Icon name="account" size="md" textColor="secondary" />
135
+ <Text typography="body2">Secondary</Text>
136
+ </View>
137
+ <View style={{ alignItems: 'center', gap: 4 }}>
138
+ <Icon name="account" size="md" textColor="tertiary" />
139
+ <Text typography="body2">Tertiary</Text>
140
+ </View>
141
+ </View>
142
+ </View>
143
+
144
+ {/* Inverse Text Colors */}
145
+ <View gap="md">
146
+ <Text typography="subtitle1">Inverse Text Colors</Text>
147
+ <View style={{ flexDirection: 'row', gap: 16, flexWrap: 'wrap', alignItems: 'center', backgroundColor: '#333', padding: 12, borderRadius: 8 }}>
148
+ <View style={{ alignItems: 'center', gap: 4 }}>
149
+ <Icon name="account" size="md" textColor="inverse" />
150
+ <Text typography="body2" color="inverse">Inverse</Text>
151
+ </View>
152
+ <View style={{ alignItems: 'center', gap: 4 }}>
153
+ <Icon name="account" size="md" textColor="inverse-secondary" />
154
+ <Text typography="body2" color="inverse-secondary">Inverse Secondary</Text>
155
+ </View>
156
+ <View style={{ alignItems: 'center', gap: 4 }}>
157
+ <Icon name="account" size="md" textColor="inverse-tertiary" />
158
+ <Text typography="body2" color="inverse-tertiary">Inverse Tertiary</Text>
159
+ </View>
160
+ </View>
161
+ </View>
162
+
125
163
  {/* Color Shades */}
126
164
  <View gap="md">
127
165
  <Text typography="subtitle1">Color Shades</Text>