@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.
- package/package.json +3 -3
- package/plugin/__tests__/web.test.ts +611 -0
- package/plugin/web.js +30 -0
- package/src/Accordion/Accordion.native.tsx +1 -1
- package/src/Accordion/Accordion.styles.tsx +3 -0
- package/src/Accordion/Accordion.web.tsx +21 -30
- package/src/Alert/Alert.styles.tsx +21 -8
- package/src/Alert/Alert.web.tsx +4 -4
- package/src/Button/Button.native.tsx +46 -20
- package/src/Button/Button.styles.tsx +15 -0
- package/src/Button/Button.web.tsx +51 -8
- package/src/Button/types.ts +7 -0
- package/src/Icon/Icon.native.tsx +2 -1
- package/src/Icon/Icon.styles.tsx +8 -4
- package/src/Icon/Icon.web.tsx +8 -4
- package/src/Icon/types.ts +34 -7
- package/src/Input/Input.styles.tsx +5 -1
- package/src/Input/Input.web.tsx +18 -12
- package/src/List/List.native.tsx +14 -2
- package/src/List/List.styles.tsx +6 -3
- package/src/List/List.web.tsx +14 -2
- package/src/List/ListItem.native.tsx +3 -2
- package/src/List/ListItem.web.tsx +3 -2
- package/src/examples/ButtonExamples.tsx +110 -0
- package/src/examples/IconExamples.tsx +38 -0
package/src/Input/Input.web.tsx
CHANGED
|
@@ -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 (
|
|
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
|
|
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
|
-
{
|
|
197
|
+
size={iconSize}
|
|
198
|
+
color={iconColor}
|
|
195
199
|
aria-label={leftIcon}
|
|
196
200
|
/>
|
|
197
201
|
);
|
|
198
202
|
} else if (isValidElement(leftIcon)) {
|
|
199
|
-
return <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
|
-
{
|
|
217
|
+
size={iconSize}
|
|
218
|
+
color={iconColor}
|
|
214
219
|
aria-label={rightIcon}
|
|
215
220
|
/>
|
|
216
221
|
);
|
|
217
222
|
} else if (isValidElement(rightIcon)) {
|
|
218
|
-
return <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
|
-
{
|
|
235
|
+
size={iconSize}
|
|
236
|
+
color={iconColor}
|
|
231
237
|
aria-label={iconName}
|
|
232
238
|
/>
|
|
233
239
|
);
|
package/src/List/List.native.tsx
CHANGED
|
@@ -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
|
-
{
|
|
76
|
+
{processedChildren}
|
|
65
77
|
</ListProvider>
|
|
66
78
|
);
|
|
67
79
|
|
package/src/List/List.styles.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
package/src/List/List.web.tsx
CHANGED
|
@@ -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
|
-
{
|
|
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>
|