@idealyst/components 1.0.40 → 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.
package/CLAUDE.md CHANGED
@@ -5,7 +5,7 @@ This file provides comprehensive component documentation for LLMs working with t
5
5
  ## Library Overview
6
6
 
7
7
  @idealyst/components is a cross-platform React/React Native component library with:
8
- - 11 core components organized into 5 categories
8
+ - 13 core components organized into 6 categories
9
9
  - Theme-based styling with Unistyles
10
10
  - Intent-based color system (primary, neutral, success, error, warning)
11
11
  - Cross-platform compatibility (React & React Native)
@@ -34,6 +34,10 @@ This file provides comprehensive component documentation for LLMs working with t
34
34
  ### Utility Components
35
35
  - **Icon**: Icon display (`name`, `size`, `color`, `intent`)
36
36
 
37
+ ### Overlay Components
38
+ - **Dialog**: Modal dialog (`open`, `onOpenChange`, `title`, `size="small|medium|large"`, `variant="default"`, `showCloseButton`, `closeOnBackdropClick`, `closeOnEscapeKey`)
39
+ - **Popover**: Contextual overlay (`open`, `onOpenChange`, `anchor`, `placement="top|bottom|left|right"`, `offset`, `closeOnClickOutside`, `closeOnEscapeKey`, `showArrow`)
40
+
37
41
  ## Intent System
38
42
 
39
43
  All components use a consistent intent-based color system:
@@ -73,6 +77,54 @@ All components use a consistent intent-based color system:
73
77
  </Card>
74
78
  ```
75
79
 
80
+ ### Dialog Usage
81
+ ```tsx
82
+ const [dialogOpen, setDialogOpen] = useState(false);
83
+
84
+ <Dialog
85
+ open={dialogOpen}
86
+ onOpenChange={setDialogOpen}
87
+ title="Confirm Action"
88
+ size="medium"
89
+ >
90
+ <View spacing="md">
91
+ <Text>Are you sure you want to proceed?</Text>
92
+ <View style={{ flexDirection: 'row', gap: 8 }}>
93
+ <Button variant="outlined" onPress={() => setDialogOpen(false)}>
94
+ Cancel
95
+ </Button>
96
+ <Button variant="contained" intent="primary" onPress={handleConfirm}>
97
+ Confirm
98
+ </Button>
99
+ </View>
100
+ </View>
101
+ </Dialog>
102
+ ```
103
+
104
+ ### Popover Usage
105
+ ```tsx
106
+ const [popoverOpen, setPopoverOpen] = useState(false);
107
+ const buttonRef = useRef<HTMLDivElement>(null);
108
+
109
+ <div ref={buttonRef} style={{ display: 'inline-block' }}>
110
+ <Button onPress={() => setPopoverOpen(true)}>
111
+ Show Menu
112
+ </Button>
113
+ </div>
114
+ <Popover
115
+ open={popoverOpen}
116
+ onOpenChange={setPopoverOpen}
117
+ anchor={buttonRef}
118
+ placement="bottom-start"
119
+ showArrow
120
+ >
121
+ <View spacing="sm">
122
+ <Button variant="text" onPress={handleAction}>Action 1</Button>
123
+ <Button variant="text" onPress={handleAction}>Action 2</Button>
124
+ </View>
125
+ </Popover>
126
+ ```
127
+
76
128
  ## Styling Guidelines
77
129
 
78
130
  1. **Use variants over manual styles** - Components provide semantic variants
@@ -85,13 +137,13 @@ All components use a consistent intent-based color system:
85
137
 
86
138
  ```tsx
87
139
  // Individual imports (recommended)
88
- import { Button, Text, View } from '@idealyst/components';
140
+ import { Button, Text, View, Dialog, Popover } from '@idealyst/components';
89
141
 
90
142
  // Documentation access
91
143
  import { componentDocs, getComponentDocs } from '@idealyst/components/docs';
92
144
 
93
145
  // Examples
94
- import { ButtonExamples } from '@idealyst/components/examples';
146
+ import { ButtonExamples, DialogExamples, PopoverExamples } from '@idealyst/components/examples';
95
147
  ```
96
148
 
97
149
  ## Key Props Reference
@@ -144,9 +196,11 @@ src/Badge/README.md
144
196
  src/Button/README.md
145
197
  src/Card/README.md
146
198
  src/Checkbox/README.md
199
+ src/Dialog/README.md
147
200
  src/Divider/README.md
148
201
  src/Icon/README.md
149
202
  src/Input/README.md
203
+ src/Popover/README.md
150
204
  src/Screen/README.md
151
205
  src/Text/README.md
152
206
  src/View/README.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/components",
3
- "version": "1.0.40",
3
+ "version": "1.0.43",
4
4
  "description": "Shared component library for React and React Native",
5
5
  "documentation": "https://github.com/your-username/idealyst-framework/tree/main/packages/components#readme",
6
6
  "main": "src/index.ts",
@@ -40,7 +40,7 @@
40
40
  "publish:npm": "npm publish"
41
41
  },
42
42
  "peerDependencies": {
43
- "@idealyst/theme": "^1.0.40",
43
+ "@idealyst/theme": "^1.0.43",
44
44
  "@mdi/js": "^7.4.47",
45
45
  "@mdi/react": "^1.6.1",
46
46
  "@react-native-vector-icons/common": "^12.0.1",
@@ -0,0 +1,91 @@
1
+ import React, { useEffect } from 'react';
2
+ import { Modal, View, Text, TouchableOpacity, TouchableWithoutFeedback, BackHandler } from 'react-native';
3
+ import { DialogProps } from './types';
4
+ import { dialogStyles } from './Dialog.styles';
5
+
6
+ const Dialog: React.FC<DialogProps> = ({
7
+ open,
8
+ onOpenChange,
9
+ title,
10
+ children,
11
+ size = 'medium',
12
+ variant = 'default',
13
+ showCloseButton = true,
14
+ closeOnBackdropClick = true,
15
+ animationType = 'fade',
16
+ style,
17
+ testID,
18
+ }) => {
19
+ // Handle Android back button
20
+ useEffect(() => {
21
+ if (!open) return;
22
+
23
+ const handleBackPress = () => {
24
+ onOpenChange(false);
25
+ return true; // Prevent default back behavior
26
+ };
27
+
28
+ const backHandler = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
29
+ return () => backHandler.remove();
30
+ }, [open, onOpenChange]);
31
+
32
+ const handleBackdropPress = () => {
33
+ if (closeOnBackdropClick) {
34
+ onOpenChange(false);
35
+ }
36
+ };
37
+
38
+ const handleClosePress = () => {
39
+ onOpenChange(false);
40
+ };
41
+
42
+ // Apply variants
43
+ dialogStyles.useVariants({
44
+ size,
45
+ variant,
46
+ });
47
+
48
+ return (
49
+ <Modal
50
+ visible={open}
51
+ transparent
52
+ animationType={animationType}
53
+ onRequestClose={() => onOpenChange(false)}
54
+ statusBarTranslucent
55
+ testID={testID}
56
+ >
57
+ <TouchableWithoutFeedback onPress={handleBackdropPress}>
58
+ <View style={dialogStyles.backdrop}>
59
+ <TouchableWithoutFeedback onPress={(e) => e.stopPropagation()}>
60
+ <View style={[dialogStyles.container, style]}>
61
+ {(title || showCloseButton) && (
62
+ <View style={dialogStyles.header}>
63
+ {title && (
64
+ <Text style={dialogStyles.title}>
65
+ {title}
66
+ </Text>
67
+ )}
68
+ {showCloseButton && (
69
+ <TouchableOpacity
70
+ style={dialogStyles.closeButton}
71
+ onPress={handleClosePress}
72
+ accessibilityLabel="Close dialog"
73
+ accessibilityRole="button"
74
+ >
75
+ <Text style={dialogStyles.closeButtonText}>×</Text>
76
+ </TouchableOpacity>
77
+ )}
78
+ </View>
79
+ )}
80
+ <View style={dialogStyles.content}>
81
+ {children}
82
+ </View>
83
+ </View>
84
+ </TouchableWithoutFeedback>
85
+ </View>
86
+ </TouchableWithoutFeedback>
87
+ </Modal>
88
+ );
89
+ };
90
+
91
+ export default Dialog;
@@ -0,0 +1,148 @@
1
+ import { StyleSheet } from 'react-native-unistyles';
2
+
3
+ export const dialogStyles = StyleSheet.create((theme) => ({
4
+ backdrop: {
5
+ position: 'absolute',
6
+ top: 0,
7
+ left: 0,
8
+ right: 0,
9
+ bottom: 0,
10
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
11
+ display: 'flex',
12
+ alignItems: 'center',
13
+ justifyContent: 'center',
14
+ zIndex: 1000,
15
+
16
+ // Web-specific styles
17
+ _web: {
18
+ position: 'fixed',
19
+ transition: 'opacity 150ms ease-out',
20
+ },
21
+ },
22
+
23
+ container: {
24
+ backgroundColor: theme.colors?.background?.primary || '#ffffff',
25
+ borderRadius: theme.borderRadius?.lg || 12,
26
+ shadowColor: '#000',
27
+ shadowOffset: {
28
+ width: 0,
29
+ height: 10,
30
+ },
31
+ shadowOpacity: 0.25,
32
+ shadowRadius: 20,
33
+ elevation: 10,
34
+ maxHeight: '90%',
35
+
36
+ variants: {
37
+ size: {
38
+ small: {
39
+ width: '90%',
40
+ maxWidth: 400,
41
+ },
42
+ medium: {
43
+ width: '90%',
44
+ maxWidth: 600,
45
+ },
46
+ large: {
47
+ width: '90%',
48
+ maxWidth: 800,
49
+ },
50
+ fullscreen: {
51
+ width: '100%',
52
+ height: '100%',
53
+ borderRadius: 0,
54
+ maxHeight: '100%',
55
+ },
56
+ },
57
+ variant: {
58
+ default: {},
59
+ alert: {
60
+ borderTopWidth: 4,
61
+ borderTopColor: theme.colors?.border?.primary || '#e5e7eb',
62
+ },
63
+ confirmation: {
64
+ borderTopWidth: 4,
65
+ borderTopColor: theme.colors?.border?.primary || '#e5e7eb',
66
+ },
67
+ },
68
+ },
69
+
70
+ // Web-specific styles
71
+ _web: {
72
+ position: 'relative',
73
+ display: 'flex',
74
+ flexDirection: 'column',
75
+ overflow: 'auto',
76
+ boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
77
+ transition: 'opacity 150ms ease-out, transform 150ms ease-out',
78
+ transformOrigin: 'center center',
79
+ },
80
+ },
81
+
82
+ header: {
83
+ borderBottomWidth: 1,
84
+ borderBottomColor: theme.colors?.border?.primary || '#e5e7eb',
85
+ display: 'flex',
86
+ flexDirection: 'row',
87
+ alignItems: 'center',
88
+ justifyContent: 'space-between',
89
+
90
+ _web: {
91
+ borderBottomStyle: 'solid',
92
+ },
93
+ },
94
+
95
+ title: {
96
+ marginLeft: theme.spacing?.lg || 12,
97
+ fontSize: 18,
98
+ paddingVertical: theme.spacing.md,
99
+ fontWeight: '600',
100
+ color: theme.colors?.text?.primary || '#111827',
101
+ flex: 1,
102
+
103
+ _web: {
104
+ paddingVertical: theme.spacing.xs,
105
+ }
106
+ },
107
+
108
+ closeButton: {
109
+ width: 32,
110
+ height: 32,
111
+ marginRight: theme.spacing?.md || 12,
112
+ borderRadius: 16,
113
+ backgroundColor: 'transparent',
114
+ border: 'none',
115
+ display: 'flex',
116
+ alignItems: 'center',
117
+ justifyContent: 'center',
118
+ cursor: 'pointer',
119
+
120
+ _web: {
121
+ _hover: {
122
+ backgroundColor: theme.colors?.background?.secondary || '#f3f4f6',
123
+ },
124
+ },
125
+ },
126
+
127
+ closeButtonText: {
128
+ fontSize: 18,
129
+ color: theme.colors?.text?.secondary || '#6b7280',
130
+ fontWeight: '500',
131
+ },
132
+
133
+ content: {
134
+ padding: theme.spacing?.lg || 16,
135
+
136
+ _web: {
137
+ overflow: 'visible',
138
+ maxHeight: 'none',
139
+ },
140
+ },
141
+
142
+ // Native-specific modal styles
143
+ modal: {
144
+ margin: 0,
145
+ justifyContent: 'center',
146
+ alignItems: 'center',
147
+ },
148
+ }));
@@ -0,0 +1,170 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { getWebProps } from 'react-native-unistyles/web';
4
+ import { DialogProps } from './types';
5
+ import { dialogStyles } from './Dialog.styles';
6
+ import Icon from '../Icon';
7
+
8
+
9
+ const Dialog: React.FC<DialogProps> = ({
10
+ open,
11
+ onOpenChange,
12
+ title,
13
+ children,
14
+ size = 'medium',
15
+ variant = 'default',
16
+ showCloseButton = true,
17
+ closeOnBackdropClick = true,
18
+ closeOnEscapeKey = true,
19
+ style,
20
+ testID,
21
+ }) => {
22
+ const dialogRef = useRef<HTMLDivElement>(null);
23
+ const previousActiveElementRef = useRef<HTMLElement | null>(null);
24
+ const [isVisible, setIsVisible] = useState(false);
25
+ const [shouldRender, setShouldRender] = useState(false);
26
+
27
+ // Handle mounting/unmounting with animation
28
+ useEffect(() => {
29
+ if (open && !shouldRender) {
30
+ // Opening sequence
31
+ setShouldRender(true);
32
+ // Use double requestAnimationFrame to ensure the DOM has fully rendered
33
+ requestAnimationFrame(() => {
34
+ requestAnimationFrame(() => {
35
+ setIsVisible(true);
36
+ });
37
+ });
38
+ } else if (!open && shouldRender) {
39
+ // Closing sequence
40
+ setIsVisible(false);
41
+ // Wait for transition to complete before unmounting
42
+ const timer = setTimeout(() => {
43
+ setShouldRender(false);
44
+ }, 150); // Match transition duration
45
+ return () => clearTimeout(timer);
46
+ }
47
+ }, [open, shouldRender]);
48
+
49
+ // Handle escape key
50
+ useEffect(() => {
51
+ if (!open || !closeOnEscapeKey) return;
52
+
53
+ const handleEscape = (event: KeyboardEvent) => {
54
+ if (event.key === 'Escape') {
55
+ onOpenChange(false);
56
+ }
57
+ };
58
+
59
+ document.addEventListener('keydown', handleEscape);
60
+ return () => document.removeEventListener('keydown', handleEscape);
61
+ }, [open, closeOnEscapeKey, onOpenChange]);
62
+
63
+ // Handle focus management
64
+ useEffect(() => {
65
+ if (open) {
66
+ // Store the currently focused element
67
+ previousActiveElementRef.current = document.activeElement as HTMLElement;
68
+
69
+ // Focus the dialog
70
+ setTimeout(() => {
71
+ dialogRef.current?.focus();
72
+ }, 0);
73
+ } else {
74
+ // Restore focus to the previously focused element
75
+ if (previousActiveElementRef.current) {
76
+ previousActiveElementRef.current.focus();
77
+ }
78
+ }
79
+ }, [open]);
80
+
81
+ // Prevent body scroll when dialog is open
82
+ useEffect(() => {
83
+ if (open) {
84
+ const originalStyle = window.getComputedStyle(document.body).overflow;
85
+ document.body.style.overflow = 'hidden';
86
+ return () => {
87
+ document.body.style.overflow = originalStyle;
88
+ };
89
+ }
90
+ }, [open]);
91
+
92
+ if (!shouldRender) return null;
93
+
94
+ const handleBackdropClick = (event: React.MouseEvent) => {
95
+ if (closeOnBackdropClick && event.target === event.currentTarget) {
96
+ onOpenChange(false);
97
+ }
98
+ };
99
+
100
+ const handleCloseClick = () => {
101
+ onOpenChange(false);
102
+ };
103
+
104
+ // Apply variants
105
+ dialogStyles.useVariants({
106
+ size,
107
+ variant,
108
+ });
109
+
110
+ const backdropProps = getWebProps([
111
+ dialogStyles.backdrop,
112
+ { opacity: isVisible ? 1 : 0 }
113
+ ]);
114
+ const containerProps = getWebProps([
115
+ dialogStyles.container,
116
+ style,
117
+ isVisible
118
+ ? { opacity: 1, transform: 'scale(1) translateY(0px)' }
119
+ : { opacity: 0, transform: 'scale(0.96) translateY(-4px)' }
120
+ ]);
121
+ const headerProps = getWebProps([dialogStyles.header]);
122
+ const titleProps = getWebProps([dialogStyles.title]);
123
+ const closeButtonProps = getWebProps([dialogStyles.closeButton]);
124
+ const contentProps = getWebProps([dialogStyles.content]);
125
+
126
+ const dialogContent = (
127
+ <div
128
+ {...backdropProps}
129
+ onClick={handleBackdropClick}
130
+ data-testid={testID}
131
+ >
132
+ <div
133
+ {...containerProps}
134
+ ref={dialogRef}
135
+ role="dialog"
136
+ aria-modal="true"
137
+ aria-labelledby={title ? 'dialog-title' : undefined}
138
+ tabIndex={-1}
139
+ onClick={(e) => e.stopPropagation()}
140
+ >
141
+ {(title || showCloseButton) && (
142
+ <div {...headerProps}>
143
+ {title && (
144
+ <h2 {...titleProps} id="dialog-title">
145
+ {title}
146
+ </h2>
147
+ )}
148
+ {showCloseButton && (
149
+ <button
150
+ {...closeButtonProps}
151
+ onClick={handleCloseClick}
152
+ aria-label="Close dialog"
153
+ type="button"
154
+ >
155
+ <Icon name="close" />
156
+ </button>
157
+ )}
158
+ </div>
159
+ )}
160
+ <div {...contentProps}>
161
+ {children}
162
+ </div>
163
+ </div>
164
+ </div>
165
+ );
166
+
167
+ return createPortal(dialogContent, document.body);
168
+ };
169
+
170
+ export default Dialog;