@idealyst/components 1.0.41 → 1.0.43

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.
@@ -0,0 +1,210 @@
1
+ # Dialog Component
2
+
3
+ A modal dialog component that creates a global overlay across the entire application. The Dialog component works consistently across web and React Native platforms.
4
+
5
+ ## Features
6
+
7
+ - **Global Overlay**: Appears above all other content including navigation bars and tab bars
8
+ - **Cross-Platform**: Uses React portals on web and Modal component on React Native
9
+ - **Accessibility**: Focus management, escape key support (web), hardware back button handling (native)
10
+ - **Theme Integration**: Supports intent-based colors and Unistyles variants
11
+ - **Flexible Sizing**: Multiple size options from small to fullscreen
12
+ - **Backdrop Interaction**: Configurable backdrop click behavior
13
+
14
+ ## Usage
15
+
16
+ ```tsx
17
+ import { Dialog, Button, Text } from '@idealyst/components';
18
+ import { useState } from 'react';
19
+
20
+ const MyComponent = () => {
21
+ const [open, setOpen] = useState(false);
22
+
23
+ return (
24
+ <>
25
+ <Button onPress={() => setOpen(true)}>
26
+ Open Dialog
27
+ </Button>
28
+
29
+ <Dialog
30
+ open={open}
31
+ onOpenChange={setOpen}
32
+ title="Dialog Title"
33
+ size="medium"
34
+ variant="default"
35
+ intent="primary"
36
+ >
37
+ <Text>Dialog content goes here</Text>
38
+ <Button onPress={() => setOpen(false)}>
39
+ Close
40
+ </Button>
41
+ </Dialog>
42
+ </>
43
+ );
44
+ };
45
+ ```
46
+
47
+ ## Props
48
+
49
+ | Prop | Type | Default | Description |
50
+ |------|------|---------|-------------|
51
+ | `open` | `boolean` | - | Whether the dialog is visible |
52
+ | `onOpenChange` | `(open: boolean) => void` | - | Called when dialog should open/close |
53
+ | `title` | `string` | - | Optional title displayed in header |
54
+ | `children` | `ReactNode` | - | Content to display inside dialog |
55
+ | `size` | `'small' \| 'medium' \| 'large' \| 'fullscreen'` | `'medium'` | Size of the dialog |
56
+ | `variant` | `'default' \| 'alert' \| 'confirmation'` | `'default'` | Visual style variant |
57
+ | `intent` | `'primary' \| 'neutral' \| 'success' \| 'error' \| 'warning'` | `'primary'` | Color scheme/semantic meaning |
58
+ | `showCloseButton` | `boolean` | `true` | Whether to show close button in header |
59
+ | `closeOnBackdropClick` | `boolean` | `true` | Whether clicking backdrop closes dialog |
60
+ | `closeOnEscapeKey` | `boolean` | `true` | Whether escape key closes dialog (web only) |
61
+ | `animationType` | `'slide' \| 'fade' \| 'none'` | `'fade'` | Animation type (native only) |
62
+ | `style` | `any` | - | Additional platform-specific styles |
63
+ | `testID` | `string` | - | Test identifier |
64
+
65
+ ## Variants
66
+
67
+ ### Size Variants
68
+ - **small**: 400px max width, suitable for simple alerts
69
+ - **medium**: 600px max width, good for forms and content
70
+ - **large**: 800px max width, for complex layouts
71
+ - **fullscreen**: Full screen coverage, removes border radius
72
+
73
+ ### Visual Variants
74
+ - **default**: Standard dialog appearance
75
+ - **alert**: Adds colored top border for alerts
76
+ - **confirmation**: Adds colored top border for confirmations
77
+
78
+ ### Intent Colors
79
+ When used with `alert` or `confirmation` variants:
80
+ - **primary**: Blue top border
81
+ - **success**: Green top border
82
+ - **error**: Red top border
83
+ - **warning**: Orange top border
84
+ - **neutral**: Gray top border
85
+
86
+ ## Platform Differences
87
+
88
+ ### Web Implementation
89
+ - Uses React portals to render into `document.body`
90
+ - Supports escape key to close
91
+ - Automatic focus management and restoration
92
+ - Click outside to close (configurable)
93
+ - Body scroll prevention when open
94
+
95
+ ### React Native Implementation
96
+ - Uses React Native's `Modal` component
97
+ - Hardware back button handling on Android
98
+ - Touch outside to close (configurable)
99
+ - Configurable animation types
100
+
101
+ ## Examples
102
+
103
+ ### Basic Dialog
104
+ ```tsx
105
+ <Dialog
106
+ open={isOpen}
107
+ onOpenChange={setIsOpen}
108
+ title="Basic Dialog"
109
+ >
110
+ <Text>Simple dialog content</Text>
111
+ </Dialog>
112
+ ```
113
+
114
+ ### Alert Dialog
115
+ ```tsx
116
+ <Dialog
117
+ open={alertOpen}
118
+ onOpenChange={setAlertOpen}
119
+ title="Important Alert"
120
+ variant="alert"
121
+ intent="warning"
122
+ >
123
+ <Text>This is an important message!</Text>
124
+ </Dialog>
125
+ ```
126
+
127
+ ### Confirmation Dialog
128
+ ```tsx
129
+ <Dialog
130
+ open={confirmOpen}
131
+ onOpenChange={setConfirmOpen}
132
+ title="Confirm Action"
133
+ variant="confirmation"
134
+ intent="error"
135
+ closeOnBackdropClick={false}
136
+ >
137
+ <Text>Are you sure you want to delete this item?</Text>
138
+ <View style={{ flexDirection: 'row', gap: 12, marginTop: 16 }}>
139
+ <Button variant="outlined" onPress={() => setConfirmOpen(false)}>
140
+ Cancel
141
+ </Button>
142
+ <Button variant="contained" intent="error" onPress={handleDelete}>
143
+ Delete
144
+ </Button>
145
+ </View>
146
+ </Dialog>
147
+ ```
148
+
149
+ ### Fullscreen Dialog
150
+ ```tsx
151
+ <Dialog
152
+ open={fullscreenOpen}
153
+ onOpenChange={setFullscreenOpen}
154
+ title="Fullscreen Dialog"
155
+ size="fullscreen"
156
+ >
157
+ <Text>This dialog covers the entire screen</Text>
158
+ </Dialog>
159
+ ```
160
+
161
+ ## Accessibility
162
+
163
+ - **Focus Management**: Automatically focuses dialog on open and restores focus on close (web)
164
+ - **Keyboard Navigation**: Escape key support on web
165
+ - **Screen Readers**: Proper ARIA roles and labels
166
+ - **Touch Targets**: Minimum 44px touch targets for interactive elements
167
+ - **Hardware Back Button**: Handled automatically on Android
168
+
169
+ ## Best Practices
170
+
171
+ 1. **Use appropriate sizes**: Choose size based on content complexity
172
+ 2. **Provide clear actions**: Always include a way to close the dialog
173
+ 3. **Use variants appropriately**: Choose alert or confirmation for important dialogs
174
+ 4. **Handle confirmation dialogs carefully**: Disable backdrop close for destructive actions
175
+ 5. **Keep content focused**: Dialogs should have a single, clear purpose
176
+ 6. **Test cross-platform**: Verify behavior on both web and native platforms
177
+
178
+ ## Common Patterns
179
+
180
+ ### Form Dialog
181
+ ```tsx
182
+ <Dialog open={formOpen} onOpenChange={setFormOpen} title="Edit Profile">
183
+ <View spacing="md">
184
+ <Input label="Name" value={name} onChangeText={setName} />
185
+ <Input label="Email" value={email} onChangeText={setEmail} />
186
+ <View style={{ flexDirection: 'row', gap: 12 }}>
187
+ <Button variant="outlined" onPress={() => setFormOpen(false)}>
188
+ Cancel
189
+ </Button>
190
+ <Button variant="contained" onPress={handleSave}>
191
+ Save
192
+ </Button>
193
+ </View>
194
+ </View>
195
+ </Dialog>
196
+ ```
197
+
198
+ ### Loading Dialog
199
+ ```tsx
200
+ <Dialog
201
+ open={loading}
202
+ onOpenChange={() => {}}
203
+ closeOnBackdropClick={false}
204
+ showCloseButton={false}
205
+ >
206
+ <View style={{ alignItems: 'center', padding: 20 }}>
207
+ <Text>Loading...</Text>
208
+ </View>
209
+ </Dialog>
210
+ ```
@@ -0,0 +1,2 @@
1
+ export { default } from './Dialog.native';
2
+ export * from './types';
@@ -0,0 +1,2 @@
1
+ export { default } from './Dialog.web';
2
+ export * from './types';
@@ -0,0 +1,2 @@
1
+ export { default } from './Dialog.web';
2
+ export * from './types';
@@ -0,0 +1,63 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ export interface DialogProps {
4
+ /**
5
+ * Whether the dialog is open/visible
6
+ */
7
+ open: boolean;
8
+
9
+ /**
10
+ * Called when the dialog should be opened or closed
11
+ */
12
+ onOpenChange: (open: boolean) => void;
13
+
14
+ /**
15
+ * Optional title for the dialog
16
+ */
17
+ title?: string;
18
+
19
+ /**
20
+ * The content to display inside the dialog
21
+ */
22
+ children: ReactNode;
23
+
24
+ /**
25
+ * The size of the dialog
26
+ */
27
+ size?: 'small' | 'medium' | 'large' | 'fullscreen';
28
+
29
+ /**
30
+ * The visual style variant of the dialog
31
+ */
32
+ variant?: 'default' | 'alert' | 'confirmation';
33
+
34
+ /**
35
+ * Whether to show the close button in the header
36
+ */
37
+ showCloseButton?: boolean;
38
+
39
+ /**
40
+ * Whether clicking the backdrop should close the dialog
41
+ */
42
+ closeOnBackdropClick?: boolean;
43
+
44
+ /**
45
+ * Whether pressing escape key should close the dialog (web only)
46
+ */
47
+ closeOnEscapeKey?: boolean;
48
+
49
+ /**
50
+ * Animation type for the dialog (native only)
51
+ */
52
+ animationType?: 'slide' | 'fade' | 'none';
53
+
54
+ /**
55
+ * Additional styles (platform-specific)
56
+ */
57
+ style?: any;
58
+
59
+ /**
60
+ * Test ID for testing
61
+ */
62
+ testID?: string;
63
+ }
@@ -3,9 +3,11 @@ import { TextInput } from 'react-native';
3
3
  import { InputProps } from './types';
4
4
  import { inputStyles } from './Input.styles';
5
5
 
6
- const Input: React.FC<InputProps> = ({
6
+ const Input = React.forwardRef<TextInput, InputProps>(({
7
7
  value,
8
8
  onChangeText,
9
+ onFocus,
10
+ onBlur,
9
11
  placeholder,
10
12
  disabled = false,
11
13
  inputType = 'text',
@@ -16,7 +18,7 @@ const Input: React.FC<InputProps> = ({
16
18
  hasError = false,
17
19
  style,
18
20
  testID,
19
- }) => {
21
+ }, ref) => {
20
22
  const [isFocused, setIsFocused] = useState(false);
21
23
 
22
24
  const getKeyboardType = () => {
@@ -34,10 +36,16 @@ const Input: React.FC<InputProps> = ({
34
36
 
35
37
  const handleFocus = () => {
36
38
  setIsFocused(true);
39
+ if (onFocus) {
40
+ onFocus();
41
+ }
37
42
  };
38
43
 
39
44
  const handleBlur = () => {
40
45
  setIsFocused(false);
46
+ if (onBlur) {
47
+ onBlur();
48
+ }
41
49
  };
42
50
 
43
51
  // Apply variants to the stylesheet
@@ -56,6 +64,7 @@ const Input: React.FC<InputProps> = ({
56
64
 
57
65
  return (
58
66
  <TextInput
67
+ ref={ref}
59
68
  value={value}
60
69
  onChangeText={onChangeText}
61
70
  placeholder={placeholder}
@@ -70,6 +79,6 @@ const Input: React.FC<InputProps> = ({
70
79
  placeholderTextColor="#999999"
71
80
  />
72
81
  );
73
- };
82
+ });
74
83
 
75
84
  export default Input;
@@ -56,6 +56,19 @@ export const inputStyles = StyleSheet.create((theme) => ({
56
56
  border: 'none',
57
57
  },
58
58
  },
59
+ bare: {
60
+ backgroundColor: 'transparent',
61
+ borderWidth: 0,
62
+ borderColor: 'transparent',
63
+ color: theme.colors?.text?.primary || '#000000',
64
+ paddingHorizontal: 0,
65
+ paddingVertical: 0,
66
+
67
+ _web: {
68
+ border: 'none',
69
+ boxShadow: 'none',
70
+ },
71
+ },
59
72
  },
60
73
  focused: {
61
74
  true: {
@@ -106,6 +119,16 @@ export const inputStyles = StyleSheet.create((theme) => ({
106
119
  elevation: 0.5,
107
120
  },
108
121
  },
122
+ // Bare variant focus (no visual changes)
123
+ {
124
+ variant: 'bare',
125
+ focused: true,
126
+ styles: {
127
+ backgroundColor: 'transparent',
128
+ borderWidth: 0,
129
+ borderColor: 'transparent',
130
+ },
131
+ },
109
132
  ],
110
133
 
111
134
  borderRadius: theme.borderRadius?.md || 8,
@@ -3,9 +3,11 @@ import { getWebProps } from 'react-native-unistyles/web';
3
3
  import { InputProps } from './types';
4
4
  import { inputStyles } from './Input.styles';
5
5
 
6
- const Input: React.FC<InputProps> = ({
6
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
7
7
  value,
8
8
  onChangeText,
9
+ onFocus,
10
+ onBlur,
9
11
  placeholder,
10
12
  disabled = false,
11
13
  inputType = 'text',
@@ -16,7 +18,8 @@ const Input: React.FC<InputProps> = ({
16
18
  hasError = false,
17
19
  style,
18
20
  testID,
19
- }) => {
21
+ }, ref) => {
22
+
20
23
  const getInputType = () => {
21
24
  switch (inputType) {
22
25
  case 'email':
@@ -37,10 +40,22 @@ const Input: React.FC<InputProps> = ({
37
40
  }
38
41
  };
39
42
 
43
+ const handleFocus = () => {
44
+ if (onFocus) {
45
+ onFocus();
46
+ }
47
+ };
48
+
49
+ const handleBlur = () => {
50
+ if (onBlur) {
51
+ onBlur();
52
+ }
53
+ };
54
+
40
55
  // Apply variants using the correct Unistyles 3.0 pattern
41
56
  inputStyles.useVariants({
42
57
  size: size as 'small' | 'medium' | 'large',
43
- variant: variant as 'default' | 'outlined' | 'filled',
58
+ variant: variant as 'default' | 'outlined' | 'filled' | 'bare',
44
59
  });
45
60
 
46
61
  // Create the style array following the official documentation pattern
@@ -51,21 +66,34 @@ const Input: React.FC<InputProps> = ({
51
66
  style,
52
67
  ].filter(Boolean);
53
68
 
54
- // Use getWebProps to generate className and ref for web
55
- const webProps = getWebProps(inputStyleArray);
69
+ // Use getWebProps for Unistyles, then manually add our ref
70
+ const { ref: unistylesRef, ...webProps } = getWebProps(inputStyleArray);
71
+
72
+ // Forward the ref while still providing unistyles with access
73
+ const handleRef = (r: HTMLInputElement | null) => {
74
+ unistylesRef.current = r;
75
+ if (typeof ref === 'function') {
76
+ ref(r);
77
+ } else if (ref) {
78
+ ref.current = r;
79
+ }
80
+ };
56
81
 
57
82
  return (
58
83
  <input
59
84
  {...webProps}
85
+ ref={handleRef}
60
86
  type={secureTextEntry ? 'password' : getInputType()}
61
87
  value={value}
62
88
  onChange={handleChange}
89
+ onFocus={handleFocus}
90
+ onBlur={handleBlur}
63
91
  placeholder={placeholder}
64
92
  disabled={disabled}
65
93
  autoCapitalize={autoCapitalize}
66
94
  data-testid={testID}
67
95
  />
68
96
  );
69
- };
97
+ });
70
98
 
71
99
  export default Input;
@@ -11,6 +11,16 @@ export interface InputProps {
11
11
  */
12
12
  onChangeText?: (text: string) => void;
13
13
 
14
+ /**
15
+ * Called when the input receives focus
16
+ */
17
+ onFocus?: () => void;
18
+
19
+ /**
20
+ * Called when the input loses focus
21
+ */
22
+ onBlur?: () => void;
23
+
14
24
  /**
15
25
  * Placeholder text
16
26
  */
@@ -44,7 +54,7 @@ export interface InputProps {
44
54
  /**
45
55
  * Style variant of the input
46
56
  */
47
- variant?: 'default' | 'outlined' | 'filled';
57
+ variant?: 'default' | 'outlined' | 'filled' | 'bare';
48
58
 
49
59
  /**
50
60
  * The intent/color scheme of the input (for focus states, validation, etc.)
@@ -0,0 +1,87 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Modal, View, TouchableWithoutFeedback, BackHandler, Dimensions } from 'react-native';
3
+ import { PopoverProps } from './types';
4
+ import { popoverStyles } from './Popover.styles';
5
+
6
+ const Popover: React.FC<PopoverProps> = ({
7
+ open,
8
+ onOpenChange,
9
+ anchor,
10
+ children,
11
+ placement = 'bottom',
12
+ offset = 8,
13
+ closeOnClickOutside = true,
14
+ showArrow = false, // Arrows are complex on native, disabled by default
15
+ style,
16
+ testID,
17
+ }) => {
18
+ const popoverRef = useRef<View>(null);
19
+
20
+ // Handle Android back button
21
+ useEffect(() => {
22
+ if (!open) return;
23
+
24
+ const handleBackPress = () => {
25
+ onOpenChange(false);
26
+ return true;
27
+ };
28
+
29
+ const backHandler = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
30
+ return () => backHandler.remove();
31
+ }, [open, onOpenChange]);
32
+
33
+ const handleBackdropPress = () => {
34
+ if (closeOnClickOutside) {
35
+ onOpenChange(false);
36
+ }
37
+ };
38
+
39
+ if (!open) return null;
40
+
41
+ // For React Native, we simplify positioning - center the popover
42
+ // More complex anchor positioning would require measuring anchor positions
43
+ // which is challenging cross-platform
44
+ const screenDimensions = Dimensions.get('window');
45
+ const popoverStyle = [
46
+ popoverStyles.container,
47
+ {
48
+ // Center on screen as a simplified approach
49
+ position: 'absolute',
50
+ top: screenDimensions.height * 0.4,
51
+ left: 20,
52
+ right: 20,
53
+ maxWidth: screenDimensions.width - 40,
54
+ },
55
+ style,
56
+ ];
57
+
58
+ return (
59
+ <Modal
60
+ visible={open}
61
+ transparent
62
+ animationType="fade"
63
+ onRequestClose={() => onOpenChange(false)}
64
+ testID={testID}
65
+ >
66
+ <TouchableWithoutFeedback onPress={handleBackdropPress}>
67
+ <View style={popoverStyles.backdrop}>
68
+ <TouchableWithoutFeedback onPress={(e) => e.stopPropagation()}>
69
+ <View ref={popoverRef} style={popoverStyle}>
70
+ {showArrow && (
71
+ <View style={[
72
+ popoverStyles.arrow,
73
+ // Apply placement-based arrow positioning
74
+ ]} />
75
+ )}
76
+ <View style={popoverStyles.content}>
77
+ {children}
78
+ </View>
79
+ </View>
80
+ </TouchableWithoutFeedback>
81
+ </View>
82
+ </TouchableWithoutFeedback>
83
+ </Modal>
84
+ );
85
+ };
86
+
87
+ export default Popover;
@@ -0,0 +1,96 @@
1
+ import { StyleSheet } from 'react-native-unistyles';
2
+
3
+ export const popoverStyles = StyleSheet.create((theme) => ({
4
+ container: {
5
+ backgroundColor: theme.colors?.surface?.primary || '#ffffff',
6
+ borderRadius: theme.borderRadius?.md || 8,
7
+ border: `1px solid ${theme.colors?.border?.primary || '#e5e7eb'}`,
8
+ boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
9
+ transition: 'opacity 150ms ease-out, transform 150ms ease-out',
10
+ transformOrigin: 'center center',
11
+ maxWidth: 320,
12
+ },
13
+
14
+ content: {
15
+ padding: theme.spacing?.md || 12,
16
+ },
17
+
18
+ arrow: {
19
+ position: 'absolute',
20
+ width: 12,
21
+ height: 12,
22
+ backgroundColor: theme.colors?.surface?.primary || '#ffffff',
23
+ transform: 'rotate(45deg)',
24
+
25
+ variants: {
26
+ placement: {
27
+ top: {
28
+ bottom: -6,
29
+ left: '50%',
30
+ marginLeft: -6,
31
+ },
32
+ 'top-start': {
33
+ bottom: -6,
34
+ left: 16,
35
+ },
36
+ 'top-end': {
37
+ bottom: -6,
38
+ right: 16,
39
+ },
40
+ bottom: {
41
+ top: -6,
42
+ left: '50%',
43
+ marginLeft: -6,
44
+ },
45
+ 'bottom-start': {
46
+ top: -6,
47
+ left: 16,
48
+ },
49
+ 'bottom-end': {
50
+ top: -6,
51
+ right: 16,
52
+ },
53
+ left: {
54
+ right: -6,
55
+ top: '50%',
56
+ marginTop: -6,
57
+ },
58
+ 'left-start': {
59
+ right: -6,
60
+ top: 16,
61
+ },
62
+ 'left-end': {
63
+ right: -6,
64
+ bottom: 16,
65
+ },
66
+ right: {
67
+ left: -6,
68
+ top: '50%',
69
+ marginTop: -6,
70
+ },
71
+ 'right-start': {
72
+ left: -6,
73
+ top: 16,
74
+ },
75
+ 'right-end': {
76
+ left: -6,
77
+ bottom: 16,
78
+ },
79
+ },
80
+ },
81
+
82
+ _web: {
83
+ boxShadow: '-2px 2px 4px rgba(0, 0, 0, 0.1)',
84
+ },
85
+ },
86
+
87
+ // Native-specific backdrop
88
+ backdrop: {
89
+ position: 'absolute',
90
+ top: 0,
91
+ left: 0,
92
+ right: 0,
93
+ bottom: 0,
94
+ backgroundColor: 'transparent',
95
+ },
96
+ }));