@idealyst/components 1.0.93 → 1.0.95

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.0.93",
3
+ "version": "1.0.95",
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",
@@ -41,7 +41,7 @@
41
41
  "publish:npm": "npm publish"
42
42
  },
43
43
  "peerDependencies": {
44
- "@idealyst/theme": "^1.0.93",
44
+ "@idealyst/theme": "^1.0.95",
45
45
  "@mdi/js": ">=7.0.0",
46
46
  "@mdi/react": ">=1.0.0",
47
47
  "@react-native-vector-icons/common": ">=12.0.0",
@@ -91,7 +91,7 @@
91
91
  }
92
92
  },
93
93
  "devDependencies": {
94
- "@idealyst/theme": "^1.0.93",
94
+ "@idealyst/theme": "^1.0.95",
95
95
  "@mdi/react": "^1.6.1",
96
96
  "@types/react": "^19.1.0",
97
97
  "react": "^19.1.0",
@@ -27,18 +27,30 @@ function getIconColor(theme: Theme, color?: Color, intent?: Intent): string {
27
27
  return theme.colors.text.primary;
28
28
  }
29
29
 
30
- function buildIconSize(theme: Theme, size: IconSizeVariant) {
31
- const iconSize = theme.sizes.icon[size];
30
+ function buildIconSize(theme: Theme, size?: IconSizeVariant) {
31
+ // Handle direct numeric sizes
32
+ if (typeof size === 'number') {
33
+ return {
34
+ width: size,
35
+ height: size,
36
+ };
37
+ }
38
+
39
+ // Default to 'md' if size is undefined
40
+ const sizeKey = size || 'md';
41
+ const iconSize = theme.sizes.icon[sizeKey];
42
+
32
43
  if (typeof iconSize === 'number') {
33
44
  return {
34
45
  width: iconSize,
35
46
  height: iconSize,
36
47
  };
37
48
  }
49
+
38
50
  return buildSizeVariants(theme, 'icon', (size) => ({
39
51
  width: size.width,
40
52
  height: size.height,
41
- }))[size];
53
+ }))[sizeKey];
42
54
  }
43
55
 
44
56
  function createIconStyles(theme: Theme) {
@@ -86,7 +86,8 @@ function createFocusedCompoundVariants(theme: Theme) {
86
86
  styles: {
87
87
  borderColor: focusColor,
88
88
  _web: {
89
- border: `2px solid ${focusColor}`,
89
+ border: `1px solid ${focusColor}`,
90
+ boxShadow: `0 0 0 2px ${focusColor}20`,
90
91
  },
91
92
  },
92
93
  });
@@ -58,7 +58,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
58
58
  }
59
59
  };
60
60
 
61
- const handleFocus = () => {
61
+ const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
62
+ e.preventDefault();
63
+ e.stopPropagation();
62
64
  setIsFocused(true);
63
65
  if (onFocus) {
64
66
  onFocus();
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { useNavigator } from '@idealyst/navigation';
3
+ import { Pressable } from '../Pressable';
4
+ import type { LinkProps } from './types';
5
+
6
+ const Link: React.FC<LinkProps> = ({
7
+ to,
8
+ vars,
9
+ children,
10
+ disabled = false,
11
+ style,
12
+ testID,
13
+ accessibilityLabel,
14
+ onPress,
15
+ }) => {
16
+ const navigator = useNavigator();
17
+
18
+ const handlePress = () => {
19
+ if (disabled) return;
20
+
21
+ onPress?.();
22
+ navigator.navigate({ path: to, vars });
23
+ };
24
+
25
+ return (
26
+ <Pressable
27
+ onPress={handlePress}
28
+ disabled={disabled}
29
+ style={style}
30
+ testID={testID}
31
+ accessibilityLabel={accessibilityLabel}
32
+ accessibilityRole="link"
33
+ >
34
+ {children}
35
+ </Pressable>
36
+ );
37
+ };
38
+
39
+ export default Link;
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { useNavigator } from '@idealyst/navigation';
3
+ import { Pressable } from '../Pressable';
4
+ import type { LinkProps } from './types';
5
+
6
+ const Link: React.FC<LinkProps> = ({
7
+ to,
8
+ vars,
9
+ children,
10
+ disabled = false,
11
+ style,
12
+ testID,
13
+ accessibilityLabel,
14
+ onPress,
15
+ }) => {
16
+ const navigator = useNavigator();
17
+
18
+ const handlePress = () => {
19
+ if (disabled) return;
20
+
21
+ onPress?.();
22
+ navigator.navigate({ path: to, vars });
23
+ };
24
+
25
+ return (
26
+ <Pressable
27
+ onPress={handlePress}
28
+ disabled={disabled}
29
+ style={style}
30
+ testID={testID}
31
+ accessibilityLabel={accessibilityLabel}
32
+ accessibilityRole="link"
33
+ >
34
+ {children}
35
+ </Pressable>
36
+ );
37
+ };
38
+
39
+ export default Link;
@@ -0,0 +1,3 @@
1
+ export { default } from './Link.native';
2
+ export { default as Link } from './Link.native';
3
+ export * from './types';
@@ -0,0 +1,5 @@
1
+ import LinkComponent from './Link.web';
2
+
3
+ export default LinkComponent;
4
+ export { LinkComponent as Link };
5
+ export * from './types';
@@ -0,0 +1,5 @@
1
+ import LinkComponent from './Link.web';
2
+
3
+ export default LinkComponent;
4
+ export { LinkComponent as Link };
5
+ export * from './types';
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { StyleProp, ViewStyle } from 'react-native';
3
+
4
+ export interface LinkProps {
5
+ /** The destination path to navigate to */
6
+ to: string;
7
+ /** Variables to substitute in the path (e.g., { id: '123' } for '/user/:id') */
8
+ vars?: Record<string, string>;
9
+ /** Content to render inside the link */
10
+ children?: ReactNode;
11
+ /** Whether the link is disabled */
12
+ disabled?: boolean;
13
+ /** Style to apply to the link container */
14
+ style?: StyleProp<ViewStyle>;
15
+ /** Test ID for testing */
16
+ testID?: string;
17
+ /** Accessibility label */
18
+ accessibilityLabel?: string;
19
+ /** Callback fired when the link is pressed */
20
+ onPress?: () => void;
21
+ }
@@ -39,21 +39,26 @@ const Pressable = forwardRef<HTMLDivElement, PressableProps>(({
39
39
  }
40
40
  }, [disabled, onPress]);
41
41
 
42
- const webStyle = {
42
+ const baseStyle: React.CSSProperties = {
43
43
  cursor: disabled ? 'default' : 'pointer',
44
44
  outline: 'none',
45
- userSelect: 'none' as const,
46
- WebkitUserSelect: 'none' as const,
45
+ userSelect: 'none',
46
+ WebkitUserSelect: 'none',
47
47
  WebkitTapHighlightColor: 'transparent',
48
48
  opacity: disabled ? 0.5 : 1,
49
49
  };
50
50
 
51
+ // Merge base styles with custom styles for web compatibility
52
+ const mergedStyle: React.CSSProperties = style
53
+ ? { ...baseStyle, ...(style as React.CSSProperties) }
54
+ : baseStyle;
55
+
51
56
  return (
52
57
  <div
53
58
  ref={ref}
54
59
  role={accessibilityRole}
55
60
  tabIndex={disabled ? -1 : 0}
56
- style={[webStyle, style] as any}
61
+ style={mergedStyle}
57
62
  onMouseDown={handleMouseDown}
58
63
  onMouseUp={handleMouseUp}
59
64
  onMouseLeave={handleMouseUp} // Handle mouse leave as press out
@@ -113,7 +113,8 @@ function buildDynamicTriggerStyles(theme: Theme) {
113
113
  true: {
114
114
  borderColor: theme.intents.primary.primary,
115
115
  _web: {
116
- border: `2px solid ${theme.intents.primary.primary}`,
116
+ border: `1px solid ${theme.intents.primary.primary}`,
117
+ boxShadow: `0 0 0 2px ${theme.intents.primary.primary}20`,
117
118
  outline: 'none',
118
119
  },
119
120
  },
@@ -109,6 +109,14 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(({
109
109
  }
110
110
  };
111
111
 
112
+ const handleTriggerFocus = () => {
113
+ if (!disabled && !isOpen) {
114
+ setIsOpen(true);
115
+ setSearchTerm('');
116
+ setFocusedIndex(-1);
117
+ }
118
+ };
119
+
112
120
  const handleOptionSelect = (option: SelectOption) => {
113
121
  if (!option.disabled) {
114
122
  onValueChange(option.value);
@@ -151,6 +159,7 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(({
151
159
  <button
152
160
  {...triggerWebProps}
153
161
  onClick={handleTriggerClick}
162
+ onFocus={handleTriggerFocus}
154
163
  disabled={disabled}
155
164
  aria-label={accessibilityLabel || label}
156
165
  aria-expanded={isOpen}
@@ -0,0 +1,212 @@
1
+ import React from 'react';
2
+ import { Screen, View, Link, Text, Button, Card, Icon } from '@idealyst/components';
3
+
4
+ export const LinkExamples = () => {
5
+ const handlePress = (linkType: string) => {
6
+ console.log(`Link pressed: ${linkType}`);
7
+ };
8
+
9
+ return (
10
+ <Screen background="primary">
11
+ <View spacing="none">
12
+ <Text size="lg" weight="bold" align="center">
13
+ Link Examples
14
+ </Text>
15
+
16
+ {/* Basic Text Links */}
17
+ <View spacing="md">
18
+ <Text size="md" weight="semibold">Basic Text Links</Text>
19
+ <View style={{ flexDirection: 'row', gap: 16, flexWrap: 'wrap' }}>
20
+ <Link to="/home" onPress={() => handlePress('home')}>
21
+ <Text color="primary">Go to Home</Text>
22
+ </Link>
23
+ <Link to="/settings" onPress={() => handlePress('settings')}>
24
+ <Text color="primary">Settings</Text>
25
+ </Link>
26
+ <Link to="/profile" onPress={() => handlePress('profile')}>
27
+ <Text color="primary">Profile</Text>
28
+ </Link>
29
+ </View>
30
+ </View>
31
+
32
+ {/* Links with Path Variables */}
33
+ <View spacing="md">
34
+ <Text size="md" weight="semibold">Links with Path Variables</Text>
35
+ <View style={{ flexDirection: 'column', gap: 8 }}>
36
+ <Link
37
+ to="/user/:id"
38
+ vars={{ id: '123' }}
39
+ onPress={() => handlePress('user-123')}
40
+ >
41
+ <Text color="primary">View User #123</Text>
42
+ </Link>
43
+ <Link
44
+ to="/product/:category/:id"
45
+ vars={{ category: 'electronics', id: '456' }}
46
+ onPress={() => handlePress('product')}
47
+ >
48
+ <Text color="primary">Electronics Product #456</Text>
49
+ </Link>
50
+ <Link
51
+ to="/post/:slug"
52
+ vars={{ slug: 'hello-world' }}
53
+ onPress={() => handlePress('post')}
54
+ >
55
+ <Text color="primary">Read "Hello World" Post</Text>
56
+ </Link>
57
+ </View>
58
+ </View>
59
+
60
+ {/* Links Wrapping Buttons */}
61
+ <View spacing="md">
62
+ <Text size="md" weight="semibold">Links Wrapping Buttons</Text>
63
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap' }}>
64
+ <Link to="/dashboard" onPress={() => handlePress('dashboard-btn')}>
65
+ <Button type="contained" intent="primary">
66
+ Go to Dashboard
67
+ </Button>
68
+ </Link>
69
+ <Link to="/create" onPress={() => handlePress('create-btn')}>
70
+ <Button type="contained" intent="success" leftIcon="plus">
71
+ Create New
72
+ </Button>
73
+ </Link>
74
+ <Link to="/help" onPress={() => handlePress('help-btn')}>
75
+ <Button type="outlined" intent="neutral">
76
+ Get Help
77
+ </Button>
78
+ </Link>
79
+ </View>
80
+ </View>
81
+
82
+ {/* Links with Icons and Text */}
83
+ <View spacing="md">
84
+ <Text size="md" weight="semibold">Links with Icons</Text>
85
+ <View style={{ flexDirection: 'column', gap: 12 }}>
86
+ <Link to="/notifications" onPress={() => handlePress('notifications')}>
87
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
88
+ <Icon name="bell" size={20} intent="primary" />
89
+ <Text color="primary">Notifications</Text>
90
+ </View>
91
+ </Link>
92
+ <Link to="/messages" onPress={() => handlePress('messages')}>
93
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
94
+ <Icon name="email" size={20} intent="primary" />
95
+ <Text color="primary">Messages</Text>
96
+ </View>
97
+ </Link>
98
+ <Link to="/favorites" onPress={() => handlePress('favorites')}>
99
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
100
+ <Icon name="heart" size={20} intent="error" />
101
+ <Text color="primary">Favorites</Text>
102
+ </View>
103
+ </Link>
104
+ </View>
105
+ </View>
106
+
107
+ {/* Disabled Links */}
108
+ <View spacing="md">
109
+ <Text size="md" weight="semibold">Disabled Links</Text>
110
+ <View style={{ flexDirection: 'column', gap: 12 }}>
111
+ <Link to="/restricted" disabled onPress={() => handlePress('disabled-text')}>
112
+ <Text color="secondary">Restricted Area (Disabled)</Text>
113
+ </Link>
114
+ <Link to="/premium" disabled onPress={() => handlePress('disabled-btn')}>
115
+ <Button type="contained" intent="primary" disabled>
116
+ Premium Feature
117
+ </Button>
118
+ </Link>
119
+ </View>
120
+ </View>
121
+
122
+ {/* Links Wrapping Cards */}
123
+ <View spacing="md">
124
+ <Text size="md" weight="semibold">Clickable Cards (Links)</Text>
125
+ <View style={{ flexDirection: 'column', gap: 12 }}>
126
+ <Link to="/article/1" onPress={() => handlePress('card-1')}>
127
+ <Card type="outlined" padding="md">
128
+ <Text size="md" weight="semibold">Article Title</Text>
129
+ <Text size="sm" color="secondary">
130
+ Click this card to read the full article...
131
+ </Text>
132
+ </Card>
133
+ </Link>
134
+ <Link to="/article/2" onPress={() => handlePress('card-2')}>
135
+ <Card type="elevated" padding="md">
136
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
137
+ <Icon name="file-document" size={24} intent="primary" />
138
+ <View style={{ flex: 1 }}>
139
+ <Text size="md" weight="semibold">Documentation</Text>
140
+ <Text size="sm" color="secondary">
141
+ View the complete documentation
142
+ </Text>
143
+ </View>
144
+ <Icon name="chevron-right" size={20} color="gray.500" />
145
+ </View>
146
+ </Card>
147
+ </Link>
148
+ </View>
149
+ </View>
150
+
151
+ {/* Navigation Menu Style */}
152
+ <View spacing="md">
153
+ <Text size="md" weight="semibold">Navigation Menu Style</Text>
154
+ <Card type="filled" padding="none">
155
+ <Link to="/menu/home" onPress={() => handlePress('menu-home')}>
156
+ <View style={{ flexDirection: 'row', alignItems: 'center', padding: 16, gap: 12 }}>
157
+ <Icon name="home" size={20} intent="primary" />
158
+ <Text style={{ flex: 1 }}>Home</Text>
159
+ <Icon name="chevron-right" size={16} color="gray.500" />
160
+ </View>
161
+ </Link>
162
+ <Link to="/menu/search" onPress={() => handlePress('menu-search')}>
163
+ <View style={{ flexDirection: 'row', alignItems: 'center', padding: 16, gap: 12 }}>
164
+ <Icon name="magnify" size={20} intent="primary" />
165
+ <Text style={{ flex: 1 }}>Search</Text>
166
+ <Icon name="chevron-right" size={16} color="gray.500" />
167
+ </View>
168
+ </Link>
169
+ <Link to="/menu/account" onPress={() => handlePress('menu-account')}>
170
+ <View style={{ flexDirection: 'row', alignItems: 'center', padding: 16, gap: 12 }}>
171
+ <Icon name="account" size={20} intent="primary" />
172
+ <Text style={{ flex: 1 }}>Account</Text>
173
+ <Icon name="chevron-right" size={16} color="gray.500" />
174
+ </View>
175
+ </Link>
176
+ </Card>
177
+ </View>
178
+
179
+ {/* Styled Links */}
180
+ <View spacing="md">
181
+ <Text size="md" weight="semibold">Custom Styled Links</Text>
182
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap' }}>
183
+ <Link
184
+ to="/styled/1"
185
+ style={{
186
+ backgroundColor: '#e3f2fd',
187
+ padding: 12,
188
+ borderRadius: 8
189
+ }}
190
+ onPress={() => handlePress('styled-1')}
191
+ >
192
+ <Text color="primary">Styled Container</Text>
193
+ </Link>
194
+ <Link
195
+ to="/styled/2"
196
+ style={{
197
+ borderWidth: 2,
198
+ borderStyle: 'solid',
199
+ borderColor: '#2196f3',
200
+ padding: 12,
201
+ borderRadius: 8
202
+ }}
203
+ onPress={() => handlePress('styled-2')}
204
+ >
205
+ <Text color="primary">Bordered Link</Text>
206
+ </Link>
207
+ </View>
208
+ </View>
209
+ </View>
210
+ </Screen>
211
+ );
212
+ };
@@ -1,6 +1,7 @@
1
1
  export { ButtonExamples } from './ButtonExamples';
2
2
  export { TextExamples } from './TextExamples';
3
3
  export { ViewExamples } from './ViewExamples';
4
+ export { LinkExamples } from './LinkExamples';
4
5
  export { InputExamples } from './InputExamples';
5
6
  export { IconExamples } from './IconExamples';
6
7
  export { CardExamples } from './CardExamples';
package/src/index.ts CHANGED
@@ -12,6 +12,9 @@ export * from './View/types';
12
12
  export { default as Pressable } from './Pressable';
13
13
  export * from './Pressable/types';
14
14
 
15
+ export { default as Link } from './Link';
16
+ export * from './Link/types';
17
+
15
18
  export { default as Input } from './Input';
16
19
  export * from './Input/types';
17
20
 
@@ -106,6 +109,7 @@ export * from './Breadcrumb/types';
106
109
  export type { ButtonProps } from './Button/types';
107
110
  export type { TextProps } from './Text/types';
108
111
  export type { ViewProps } from './View/types';
112
+ export type { LinkProps } from './Link/types';
109
113
  export type { InputProps } from './Input/types';
110
114
  export type { CheckboxProps } from './Checkbox/types';
111
115
  export type { CardProps } from './Card/types';