@idealyst/components 1.2.33 → 1.2.34

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.2.33",
3
+ "version": "1.2.34",
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",
@@ -56,7 +56,7 @@
56
56
  "publish:npm": "npm publish"
57
57
  },
58
58
  "peerDependencies": {
59
- "@idealyst/theme": "^1.2.33",
59
+ "@idealyst/theme": "^1.2.34",
60
60
  "@mdi/js": ">=7.0.0",
61
61
  "@mdi/react": ">=1.0.0",
62
62
  "@react-native-vector-icons/common": ">=12.0.0",
@@ -106,7 +106,7 @@
106
106
  }
107
107
  },
108
108
  "devDependencies": {
109
- "@idealyst/theme": "^1.2.33",
109
+ "@idealyst/theme": "^1.2.34",
110
110
  "@idealyst/tooling": "^1.2.30",
111
111
  "@mdi/react": "^1.6.1",
112
112
  "@types/react": "^19.1.0",
@@ -17,6 +17,7 @@ const Card = forwardRef<IdealystElement, CardProps>(({
17
17
  onPress,
18
18
  onClick,
19
19
  disabled = false,
20
+ onLayout,
20
21
  // Spacing variants from ContainerStyleProps
21
22
  gap,
22
23
  padding,
@@ -93,6 +94,7 @@ const Card = forwardRef<IdealystElement, CardProps>(({
93
94
  nativeID: id,
94
95
  style: [cardStyle, style],
95
96
  testID,
97
+ onLayout,
96
98
  ...nativeA11yProps,
97
99
  ...(clickable && {
98
100
  onPress: disabled ? undefined : pressHandler,
@@ -3,6 +3,7 @@ import { getWebProps } from 'react-native-unistyles/web';
3
3
  import { CardProps } from './types';
4
4
  import { cardStyles } from './Card.styles';
5
5
  import useMergeRefs from '../hooks/useMergeRefs';
6
+ import { useWebLayout } from '../hooks/useWebLayout';
6
7
  import { getWebInteractiveAriaProps } from '../utils/accessibility';
7
8
  import type { IdealystElement } from '../utils/refTypes';
8
9
 
@@ -23,6 +24,7 @@ const Card = forwardRef<IdealystElement, CardProps>(({
23
24
  onPress,
24
25
  onClick,
25
26
  disabled = false,
27
+ onLayout,
26
28
  // Spacing variants from ContainerStyleProps
27
29
  gap,
28
30
  padding,
@@ -43,6 +45,7 @@ const Card = forwardRef<IdealystElement, CardProps>(({
43
45
  accessibilityPressed,
44
46
  }, ref) => {
45
47
  const hasWarnedRef = useRef(false);
48
+ const layoutRef = useWebLayout<HTMLElement>(onLayout);
46
49
 
47
50
  // Warn about onClick usage (deprecated)
48
51
  useEffect(() => {
@@ -105,7 +108,7 @@ const Card = forwardRef<IdealystElement, CardProps>(({
105
108
  // Generate web props
106
109
  const webProps = getWebProps([cardStyle, style as any]);
107
110
 
108
- const mergedRef = useMergeRefs(ref, webProps.ref);
111
+ const mergedRef = useMergeRefs(ref, webProps.ref, layoutRef);
109
112
 
110
113
  // Use appropriate HTML element based on clickable state
111
114
  const Component: any = clickable ? 'button' : 'div';
package/src/Card/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Intent, Size } from '@idealyst/theme';
2
2
  import type { ReactNode } from 'react';
3
- import type { StyleProp, ViewStyle } from 'react-native';
3
+ import type { StyleProp, ViewStyle, LayoutChangeEvent } from 'react-native';
4
4
  import { ContainerStyleProps } from '../utils/viewStyleProps';
5
5
  import { InteractiveAccessibilityProps } from '../utils/accessibility';
6
6
 
@@ -78,4 +78,10 @@ export interface CardProps extends ContainerStyleProps, InteractiveAccessibility
78
78
  * Test ID for testing
79
79
  */
80
80
  testID?: string;
81
+
82
+ /**
83
+ * Called when the layout of the card changes.
84
+ * Provides the new width, height, x, and y coordinates.
85
+ */
86
+ onLayout?: (event: LayoutChangeEvent) => void;
81
87
  }
@@ -11,6 +11,7 @@ const Screen = forwardRef<IdealystElement, ScreenProps>(({
11
11
  safeArea = true,
12
12
  scrollable = true,
13
13
  contentInset,
14
+ onLayout,
14
15
  // Spacing variants from ContainerStyleProps
15
16
  gap,
16
17
  padding,
@@ -66,6 +67,7 @@ const Screen = forwardRef<IdealystElement, ScreenProps>(({
66
67
  style={[screenStyle, style]}
67
68
  contentContainerStyle={{ flexGrow: 1 }}
68
69
  testID={testID}
70
+ onLayout={onLayout}
69
71
  >
70
72
  <RNView style={[contentInsetStyle, { flex: 1 }]}>
71
73
  {children}
@@ -75,7 +77,7 @@ const Screen = forwardRef<IdealystElement, ScreenProps>(({
75
77
  }
76
78
 
77
79
  return (
78
- <RNView ref={ref as any} nativeID={id} style={[screenStyle, safeAreaStyle, style]} testID={testID}>
80
+ <RNView ref={ref as any} nativeID={id} style={[screenStyle, safeAreaStyle, style]} testID={testID} onLayout={onLayout}>
79
81
  {children}
80
82
  </RNView>
81
83
  );
@@ -3,6 +3,7 @@ import { getWebProps } from 'react-native-unistyles/web';
3
3
  import { ScreenProps } from './types';
4
4
  import { screenStyles } from './Screen.styles';
5
5
  import useMergeRefs from '../hooks/useMergeRefs';
6
+ import { useWebLayout } from '../hooks/useWebLayout';
6
7
  import type { IdealystElement } from '../utils/refTypes';
7
8
 
8
9
  /**
@@ -13,6 +14,7 @@ const Screen = forwardRef<IdealystElement, ScreenProps>(({
13
14
  children,
14
15
  background = 'screen',
15
16
  safeArea = false,
17
+ onLayout,
16
18
  // Spacing variants from ContainerStyleProps
17
19
  gap,
18
20
  padding,
@@ -25,6 +27,8 @@ const Screen = forwardRef<IdealystElement, ScreenProps>(({
25
27
  testID,
26
28
  id,
27
29
  }, ref) => {
30
+ const layoutRef = useWebLayout<HTMLDivElement>(onLayout);
31
+
28
32
  screenStyles.useVariants({
29
33
  background,
30
34
  safeArea,
@@ -40,7 +44,7 @@ const Screen = forwardRef<IdealystElement, ScreenProps>(({
40
44
  // Call style as function to get theme-reactive styles
41
45
  const webProps = getWebProps([(screenStyles.screen as any)({}), style as any]);
42
46
 
43
- const mergedRef = useMergeRefs(ref, webProps.ref);
47
+ const mergedRef = useMergeRefs(ref, webProps.ref, layoutRef);
44
48
 
45
49
  return (
46
50
  <div
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { StyleProp, ViewStyle } from 'react-native';
2
+ import type { StyleProp, ViewStyle, LayoutChangeEvent } from 'react-native';
3
3
  import { Surface } from '@idealyst/theme';
4
4
  import { ContainerStyleProps } from '../utils/viewStyleProps';
5
5
 
@@ -49,4 +49,10 @@ export interface ScreenProps extends ContainerStyleProps {
49
49
  * Scrollable content
50
50
  */
51
51
  scrollable?: boolean;
52
+
53
+ /**
54
+ * Called when the layout of the screen changes.
55
+ * Provides the new width, height, x, and y coordinates.
56
+ */
57
+ onLayout?: (event: LayoutChangeEvent) => void;
52
58
  }
@@ -45,6 +45,7 @@ const View = forwardRef<IdealystElement, ViewProps>(({
45
45
  style,
46
46
  testID,
47
47
  id,
48
+ onLayout,
48
49
  }, ref) => {
49
50
  // Set active variants for this render
50
51
  viewStyles.useVariants({
@@ -89,6 +90,7 @@ const View = forwardRef<IdealystElement, ViewProps>(({
89
90
  contentContainerStyle={[viewStyle, overrideStyles]}
90
91
  testID={testID}
91
92
  nativeID={id}
93
+ onLayout={onLayout}
92
94
  >
93
95
  {children}
94
96
  </RNScrollView>
@@ -96,7 +98,7 @@ const View = forwardRef<IdealystElement, ViewProps>(({
96
98
  }
97
99
 
98
100
  return (
99
- <RNView ref={ref as any} style={[viewStyle, overrideStyles, finalStyle]} testID={testID} nativeID={id}>
101
+ <RNView ref={ref as any} style={[viewStyle, overrideStyles, finalStyle]} testID={testID} nativeID={id} onLayout={onLayout}>
100
102
  {children}
101
103
  </RNView>
102
104
  );
@@ -3,6 +3,7 @@ import { getWebProps } from 'react-native-unistyles/web';
3
3
  import { ViewProps } from './types';
4
4
  import { viewStyles } from './View.styles';
5
5
  import useMergeRefs from '../hooks/useMergeRefs';
6
+ import { useWebLayout } from '../hooks/useWebLayout';
6
7
  import type { IdealystElement } from '../utils/refTypes';
7
8
 
8
9
  /**
@@ -31,7 +32,10 @@ const View = forwardRef<IdealystElement, ViewProps>(({
31
32
  style,
32
33
  testID,
33
34
  id,
35
+ onLayout,
34
36
  }, ref) => {
37
+ const layoutRef = useWebLayout<HTMLDivElement>(onLayout);
38
+
35
39
  viewStyles.useVariants({
36
40
  background,
37
41
  radius,
@@ -51,7 +55,7 @@ const View = forwardRef<IdealystElement, ViewProps>(({
51
55
  /** @ts-ignore */
52
56
  const wrapperWebProps = getWebProps(viewStyles.scrollableWrapper);
53
57
 
54
- const mergedRef = useMergeRefs(ref, webProps.ref);
58
+ const mergedRef = useMergeRefs(ref, webProps.ref, layoutRef);
55
59
 
56
60
  // When scrollable, render a wrapper + content structure
57
61
  // Wrapper: sizing and margin (positioning in parent layout)
package/src/View/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Size, Surface, ResponsiveStyle } from '@idealyst/theme';
2
2
  import type { ReactNode } from 'react';
3
- import type { StyleProp, ViewStyle } from 'react-native';
3
+ import type { StyleProp, ViewStyle, LayoutChangeEvent } from 'react-native';
4
4
  import { ContainerStyleProps } from '../utils/viewStyleProps';
5
5
 
6
6
  /**
@@ -99,4 +99,11 @@ export interface ViewProps extends ContainerStyleProps {
99
99
  * Test ID for testing
100
100
  */
101
101
  testID?: string;
102
+
103
+ /**
104
+ * Callback when the view's layout changes.
105
+ * Called with layout information (x, y, width, height) when the component
106
+ * mounts or when its dimensions change.
107
+ */
108
+ onLayout?: (event: LayoutChangeEvent) => void;
102
109
  }
@@ -1,3 +1,4 @@
1
+ import { useState } from 'react';
1
2
  import { Screen, View, Text, Card, Button } from '../index';
2
3
 
3
4
  export const CardExamples = () => {
@@ -5,6 +6,8 @@ export const CardExamples = () => {
5
6
  console.log(`Card pressed: ${cardType}`);
6
7
  };
7
8
 
9
+ const [cardDimensions, setCardDimensions] = useState({ width: 0, height: 0 });
10
+
8
11
  return (
9
12
  <Screen background="primary" padding="lg">
10
13
  <View gap="xl">
@@ -163,6 +166,30 @@ export const CardExamples = () => {
163
166
  </View>
164
167
  </Card>
165
168
  </View>
169
+
170
+ {/* onLayout Example */}
171
+ <View gap="md">
172
+ <Text typography="subtitle1">onLayout Callback</Text>
173
+ <Text typography="caption" color="secondary">
174
+ Track card dimensions as they change
175
+ </Text>
176
+ <Card
177
+ type="outlined"
178
+ padding="md"
179
+ onLayout={(event) => {
180
+ const { width, height } = event.nativeEvent.layout;
181
+ setCardDimensions({ width, height });
182
+ }}
183
+ >
184
+ <Text weight="semibold">Responsive Card</Text>
185
+ <Text typography="caption" color="secondary">
186
+ Width: {Math.round(cardDimensions.width)}px
187
+ </Text>
188
+ <Text typography="caption" color="secondary">
189
+ Height: {Math.round(cardDimensions.height)}px
190
+ </Text>
191
+ </Card>
192
+ </View>
166
193
  </View>
167
194
  </Screen>
168
195
  );
@@ -1,6 +1,9 @@
1
+ import { useState } from 'react';
1
2
  import { Screen, View, Text } from '../index';
2
3
 
3
4
  export const ScreenExamples = () => {
5
+ const [screenDimensions, setScreenDimensions] = useState({ width: 0, height: 0 });
6
+
4
7
  return (
5
8
  <Screen background="primary" padding="lg">
6
9
  <View gap="lg">
@@ -147,6 +150,34 @@ export const ScreenExamples = () => {
147
150
  </Screen>
148
151
  </View>
149
152
  </View>
153
+
154
+ {/* onLayout Example */}
155
+ <View gap="md">
156
+ <Text typography="subtitle1">onLayout Callback</Text>
157
+ <Text typography="caption" color="secondary">
158
+ Track screen dimensions as they change
159
+ </Text>
160
+ <View style={{ height: 120, borderWidth: 1, borderColor: '#ccc' }}>
161
+ <Screen
162
+ background="secondary"
163
+ padding="md"
164
+ onLayout={(event) => {
165
+ const { width, height } = event.nativeEvent.layout;
166
+ setScreenDimensions({ width, height });
167
+ }}
168
+ >
169
+ <View style={{ alignItems: 'center', justifyContent: 'center', flex: 1 }}>
170
+ <Text weight="semibold">Responsive Screen</Text>
171
+ <Text typography="caption" color="secondary">
172
+ Width: {Math.round(screenDimensions.width)}px
173
+ </Text>
174
+ <Text typography="caption" color="secondary">
175
+ Height: {Math.round(screenDimensions.height)}px
176
+ </Text>
177
+ </View>
178
+ </Screen>
179
+ </View>
180
+ </View>
150
181
  </View>
151
182
  </Screen>
152
183
  );
@@ -0,0 +1 @@
1
+ export { useWebLayout } from './useWebLayout.native';
@@ -0,0 +1 @@
1
+ export { useWebLayout } from './useWebLayout';
@@ -0,0 +1 @@
1
+ export { useWebLayout } from './useWebLayout.web';
@@ -0,0 +1,15 @@
1
+ import { useRef } from 'react';
2
+ import type { LayoutChangeEvent } from 'react-native';
3
+
4
+ /**
5
+ * No-op hook for native - onLayout is handled natively by React Native components.
6
+ * Returns a ref for API compatibility, but it's not used on native.
7
+ *
8
+ * @param _onLayout - Unused on native (React Native components handle onLayout directly)
9
+ * @returns A ref (unused on native, for API compatibility)
10
+ */
11
+ export function useWebLayout<T = any>(
12
+ _onLayout: ((event: LayoutChangeEvent) => void) | undefined
13
+ ) {
14
+ return useRef<T>(null);
15
+ }
@@ -0,0 +1,60 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import type { LayoutChangeEvent } from 'react-native';
3
+
4
+ /**
5
+ * Hook that provides onLayout functionality for web components using ResizeObserver.
6
+ * Returns a ref that should be attached to the element you want to observe.
7
+ *
8
+ * @param onLayout - Callback fired when layout changes, with React Native compatible event shape
9
+ * @returns A ref to attach to the observed element
10
+ */
11
+ export function useWebLayout<T extends HTMLElement = HTMLElement>(
12
+ onLayout: ((event: LayoutChangeEvent) => void) | undefined
13
+ ) {
14
+ const ref = useRef<T>(null);
15
+
16
+ useEffect(() => {
17
+ if (!onLayout || !ref.current) return;
18
+
19
+ const element = ref.current;
20
+
21
+ // Call immediately with initial layout
22
+ const rect = element.getBoundingClientRect();
23
+ onLayout({
24
+ nativeEvent: {
25
+ layout: {
26
+ x: rect.left,
27
+ y: rect.top,
28
+ width: rect.width,
29
+ height: rect.height,
30
+ },
31
+ },
32
+ } as LayoutChangeEvent);
33
+
34
+ // Set up ResizeObserver for subsequent changes
35
+ const resizeObserver = new ResizeObserver((entries) => {
36
+ for (const entry of entries) {
37
+ const { width, height } = entry.contentRect;
38
+ const boundingRect = element.getBoundingClientRect();
39
+ onLayout({
40
+ nativeEvent: {
41
+ layout: {
42
+ x: boundingRect.left,
43
+ y: boundingRect.top,
44
+ width,
45
+ height,
46
+ },
47
+ },
48
+ } as LayoutChangeEvent);
49
+ }
50
+ });
51
+
52
+ resizeObserver.observe(element);
53
+
54
+ return () => {
55
+ resizeObserver.disconnect();
56
+ };
57
+ }, [onLayout]);
58
+
59
+ return ref;
60
+ }