@idealyst/components 1.2.14 → 1.2.16

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.
Files changed (50) hide show
  1. package/package.json +3 -3
  2. package/src/Accordion/Accordion.web.tsx +1 -1
  3. package/src/Alert/Alert.native.tsx +1 -1
  4. package/src/Alert/Alert.web.tsx +1 -1
  5. package/src/Badge/Badge.web.tsx +6 -2
  6. package/src/Badge/types.ts +5 -0
  7. package/src/Button/Button.native.tsx +3 -3
  8. package/src/Button/Button.web.tsx +5 -1
  9. package/src/Button/types.ts +5 -0
  10. package/src/Card/Card.web.tsx +4 -1
  11. package/src/Card/types.ts +5 -0
  12. package/src/Dialog/Dialog.native.tsx +1 -1
  13. package/src/Divider/Divider.web.tsx +2 -2
  14. package/src/Icon/Icon.web.tsx +2 -2
  15. package/src/Image/Image.styles.tsx +5 -5
  16. package/src/Image/Image.web.tsx +3 -3
  17. package/src/List/List.native.tsx +1 -2
  18. package/src/List/List.web.tsx +1 -2
  19. package/src/List/ListSection.web.tsx +3 -3
  20. package/src/Menu/Menu.web.tsx +8 -10
  21. package/src/Menu/MenuItem.web.tsx +1 -1
  22. package/src/Popover/Popover.web.tsx +1 -1
  23. package/src/Pressable/Pressable.web.tsx +1 -1
  24. package/src/Progress/Progress.styles.tsx +76 -30
  25. package/src/Progress/Progress.web.tsx +13 -15
  26. package/src/SVGImage/SVGImage.web.tsx +1 -1
  27. package/src/Select/Select.web.tsx +2 -2
  28. package/src/Slider/Slider.styles.tsx +131 -44
  29. package/src/Slider/Slider.web.tsx +22 -22
  30. package/src/Text/Text.web.tsx +29 -3
  31. package/src/Text/types.ts +14 -1
  32. package/src/TextArea/TextArea.styles.tsx +96 -57
  33. package/src/TextArea/TextArea.web.tsx +19 -28
  34. package/src/Tooltip/Tooltip.web.tsx +3 -3
  35. package/src/Video/Video.styles.tsx +3 -3
  36. package/src/Video/Video.web.tsx +1 -1
  37. package/src/View/View.styles.tsx +2 -2
  38. package/src/View/View.web.tsx +95 -9
  39. package/src/View/types.ts +5 -1
  40. package/src/examples/ViewExamples.tsx +34 -0
  41. package/src/extensions/index.ts +0 -7
  42. package/src/hooks/useMergeRefs.ts +12 -6
  43. package/src/utils/accessibility/keyboardPatterns.ts +4 -0
  44. package/src/utils/accessibility/types.ts +5 -1
  45. package/src/utils/accessibility/useAnnounce.ts +1 -1
  46. package/src/utils/accessibility/useKeyboardNavigation.ts +1 -1
  47. package/src/utils/index.ts +0 -3
  48. package/src/utils/viewStyleProps.ts +2 -0
  49. package/src/extensions/applyExtension.ts +0 -210
  50. package/src/utils/buildSizeVariants.ts +0 -16
@@ -1,8 +1,8 @@
1
1
  /**
2
- * TextArea styles using defineStyle with dynamic props.
2
+ * TextArea styles using static styles with variants.
3
3
  */
4
4
  import { StyleSheet } from 'react-native-unistyles';
5
- import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
5
+ import { defineStyle, ThemeStyleWrapper, CompoundVariants } from '@idealyst/theme';
6
6
  import type { Theme as BaseTheme, Intent, Size } from '@idealyst/theme';
7
7
  import { ViewStyleSize } from '../utils/viewStyleProps';
8
8
 
@@ -12,26 +12,32 @@ void StyleSheet;
12
12
  // Wrap theme for $iterator support
13
13
  type Theme = ThemeStyleWrapper<BaseTheme>;
14
14
 
15
- type ResizeMode = 'none' | 'vertical' | 'horizontal' | 'both';
16
-
17
- export type TextAreaDynamicProps = {
18
- size?: Size;
19
- intent?: Intent;
20
- disabled?: boolean;
21
- hasError?: boolean;
22
- resize?: ResizeMode;
23
- isNearLimit?: boolean;
24
- isAtLimit?: boolean;
15
+ export type TextAreaVariants = {
16
+ size: Size;
17
+ intent: Intent;
18
+ disabled: boolean;
19
+ hasError: boolean;
20
+ isNearLimit: boolean;
21
+ isAtLimit: boolean;
25
22
  margin?: ViewStyleSize;
26
23
  marginVertical?: ViewStyleSize;
27
24
  marginHorizontal?: ViewStyleSize;
28
25
  };
29
26
 
27
+ // Create intent variants dynamically from theme
28
+ function createIntentVariants(theme: Theme) {
29
+ const variants: Record<string, object> = {};
30
+ for (const intent in theme.intents) {
31
+ variants[intent] = {};
32
+ }
33
+ return variants;
34
+ }
35
+
30
36
  /**
31
- * TextArea styles with intent/disabled/error handling.
37
+ * TextArea styles with static styles and variants.
32
38
  */
33
39
  export const textAreaStyles = defineStyle('TextArea', (theme: Theme) => ({
34
- container: (_props: TextAreaDynamicProps) => ({
40
+ container: {
35
41
  display: 'flex' as const,
36
42
  flexDirection: 'column' as const,
37
43
  gap: 4,
@@ -49,70 +55,103 @@ export const textAreaStyles = defineStyle('TextArea', (theme: Theme) => ({
49
55
  marginHorizontal: theme.sizes.$view.padding,
50
56
  },
51
57
  },
52
- }),
58
+ },
53
59
 
54
- label: ({ disabled = false }: TextAreaDynamicProps) => ({
60
+ label: {
55
61
  fontSize: 14,
56
62
  fontWeight: '500' as const,
57
63
  color: theme.colors.text.primary,
58
- }),
64
+ variants: {
65
+ disabled: {
66
+ true: { opacity: 0.5 },
67
+ false: { opacity: 1 },
68
+ },
69
+ },
70
+ },
59
71
 
60
- textareaContainer: (_props: TextAreaDynamicProps) => ({
72
+ textareaContainer: {
61
73
  position: 'relative' as const,
62
74
  variants: {
75
+ disabled: {
76
+ true: { opacity: 0.8 },
77
+ false: { opacity: 1 },
78
+ },
79
+ },
80
+ },
81
+
82
+ textarea: {
83
+ width: '100%',
84
+ color: theme.colors.text.primary,
85
+ variants: {
86
+ size: {
87
+ fontSize: theme.sizes.$textarea.fontSize,
88
+ padding: theme.sizes.$textarea.padding,
89
+ lineHeight: theme.sizes.$textarea.lineHeight,
90
+ minHeight: theme.sizes.$textarea.minHeight,
91
+ },
63
92
  disabled: {
64
93
  true: {
65
- opacity: 1,
94
+ opacity: 0.5,
95
+ _web: {
96
+ cursor: 'not-allowed',
97
+ },
66
98
  },
67
99
  false: {
68
- opacity: 0.8,
69
- },
70
- },
71
- }
72
- }),
73
-
74
- textarea: ({ disabled = false, resize = 'none' }: TextAreaDynamicProps) => ({
75
- width: '100%',
76
- color: theme.colors.text.primary,
77
- opacity: disabled ? 0.5 : 1,
78
- variants: {
79
- size: {
80
- fontSize: theme.sizes.$textarea.fontSize,
81
- padding: theme.sizes.$textarea.padding,
82
- lineHeight: theme.sizes.$textarea.lineHeight,
83
- minHeight: theme.sizes.$textarea.minHeight,
100
+ opacity: 1,
101
+ _web: {
102
+ cursor: 'text',
103
+ },
84
104
  },
85
105
  },
86
- _web: {
87
- fontFamily: 'inherit',
88
- outline: 'none',
89
- transition: 'border-color 0.2s ease, box-shadow 0.2s ease',
90
- boxSizing: 'border-box',
91
- overflowY: 'hidden',
92
- cursor: disabled ? 'not-allowed' : 'text',
93
- resize: resize,
94
- },
95
- }),
106
+ },
107
+ _web: {
108
+ fontFamily: 'inherit',
109
+ outline: 'none',
110
+ transition: 'border-color 0.2s ease, box-shadow 0.2s ease',
111
+ boxSizing: 'border-box',
112
+ overflowY: 'hidden',
113
+ },
114
+ },
96
115
 
97
- helperText: ({ hasError = false }: TextAreaDynamicProps) => ({
116
+ helperText: {
98
117
  fontSize: 12,
99
- color: hasError ? theme.intents.error.primary : theme.colors.text.secondary,
100
- }),
118
+ variants: {
119
+ hasError: {
120
+ true: { color: theme.intents.error.primary },
121
+ false: { color: theme.colors.text.secondary },
122
+ },
123
+ },
124
+ },
101
125
 
102
- footer: (_props: TextAreaDynamicProps) => ({
126
+ footer: {
103
127
  display: 'flex' as const,
104
128
  flexDirection: 'row' as const,
105
129
  justifyContent: 'space-between' as const,
106
130
  alignItems: 'center' as const,
107
131
  gap: 4,
108
- }),
132
+ },
109
133
 
110
- characterCount: ({ isNearLimit = false, isAtLimit = false }: TextAreaDynamicProps) => ({
134
+ characterCount: {
111
135
  fontSize: 12,
112
- color: isAtLimit
113
- ? theme.intents.error.primary
114
- : isNearLimit
115
- ? theme.intents.warning.primary
116
- : theme.colors.text.secondary,
117
- }),
136
+ color: theme.colors.text.secondary,
137
+ variants: {
138
+ isAtLimit: {
139
+ true: { color: theme.intents.error.primary },
140
+ false: {},
141
+ },
142
+ isNearLimit: {
143
+ true: {},
144
+ false: {},
145
+ },
146
+ },
147
+ compoundVariants: [
148
+ {
149
+ isNearLimit: true,
150
+ isAtLimit: false,
151
+ styles: {
152
+ color: theme.intents.warning.primary,
153
+ },
154
+ },
155
+ ] as CompoundVariants<keyof TextAreaVariants>,
156
+ },
118
157
  }));
@@ -117,37 +117,29 @@ const TextArea = forwardRef<HTMLDivElement, TextAreaProps>(({
117
117
  accessibilityAutoComplete,
118
118
  ]);
119
119
 
120
+ const isNearLimit = maxLength ? value.length >= maxLength * 0.9 : false;
121
+ const isAtLimit = maxLength ? value.length >= maxLength : false;
122
+
120
123
  // Apply variants
121
124
  textAreaStyles.useVariants({
122
125
  size,
123
126
  intent,
124
127
  disabled,
125
128
  hasError,
126
- resize,
127
- isNearLimit: maxLength ? value.length >= maxLength * 0.9 : false,
128
- isAtLimit: maxLength ? value.length >= maxLength : false,
129
+ isNearLimit,
130
+ isAtLimit,
129
131
  margin,
130
132
  marginVertical,
131
133
  marginHorizontal,
132
134
  });
133
135
 
134
- // Get dynamic styles - call as functions for theme reactivity
135
- const containerStyle = (textAreaStyles.container as any)({});
136
- const labelStyle = (textAreaStyles.label as any)({ disabled });
137
- const textareaContainerStyle = (textAreaStyles.textareaContainer as any)({});
138
- const footerStyle = (textAreaStyles.footer as any)({});
139
- const helperTextStyle = (textAreaStyles.helperText as any)({ hasError });
140
- const characterCountStyle = (textAreaStyles.characterCount as any)({
141
- isNearLimit: maxLength ? value.length >= maxLength * 0.9 : false,
142
- isAtLimit: maxLength ? value.length >= maxLength : false,
143
- });
144
-
145
- const containerProps = getWebProps([containerStyle, style as any]);
146
- const labelProps = getWebProps([labelStyle]);
147
- const textareaContainerProps = getWebProps([textareaContainerStyle]);
148
- const footerProps = getWebProps([footerStyle]);
149
- const helperTextProps = getWebProps([helperTextStyle]);
150
- const characterCountProps = getWebProps([characterCountStyle]);
136
+ // Get static styles (cast to any for Unistyles variant compatibility)
137
+ const containerProps = getWebProps([textAreaStyles.container as any, style as any]);
138
+ const labelProps = getWebProps([textAreaStyles.label as any]);
139
+ const textareaContainerProps = getWebProps([textAreaStyles.textareaContainer as any]);
140
+ const footerProps = getWebProps([textAreaStyles.footer as any]);
141
+ const helperTextProps = getWebProps([textAreaStyles.helperText as any]);
142
+ const characterCountProps = getWebProps([textAreaStyles.characterCount as any]);
151
143
 
152
144
  const adjustHeight = () => {
153
145
  if (!autoGrow || !textareaRef.current) return;
@@ -189,16 +181,15 @@ const TextArea = forwardRef<HTMLDivElement, TextAreaProps>(({
189
181
  };
190
182
 
191
183
  const showFooter = (error || helperText) || (showCharacterCount && maxLength);
192
- const isNearLimit = maxLength ? value.length >= maxLength * 0.9 : false;
193
- const isAtLimit = maxLength ? value.length >= maxLength : false;
194
184
 
195
185
  const computedTextareaProps = getWebProps([
196
- textAreaStyles.textarea({ intent, disabled, hasError }),
197
- textareaStyle,
198
- minHeight && { minHeight: `${minHeight}px` },
199
- maxHeight && { maxHeight: `${maxHeight}px` },
200
- autoGrow && maxHeight && textareaRef.current && textareaRef.current.scrollHeight > maxHeight && { overflowY: 'auto' as const },
201
- ]);
186
+ textAreaStyles.textarea as any,
187
+ textareaStyle as any,
188
+ { resize } as any, // Apply resize as inline style since it's CSS-only
189
+ minHeight ? { minHeight: `${minHeight}px` } : null,
190
+ maxHeight ? { maxHeight: `${maxHeight}px` } : null,
191
+ autoGrow && maxHeight && textareaRef.current && textareaRef.current.scrollHeight > maxHeight ? { overflowY: 'auto' as const } : null,
192
+ ].filter(Boolean));
202
193
 
203
194
  const mergedRef = useMergeRefs(ref, containerProps.ref);
204
195
  const mergedTextareaRef = useMergeRefs(textareaRef, computedTextareaProps.ref);
@@ -27,7 +27,7 @@ const Tooltip: React.FC<TooltipProps> = ({
27
27
  accessibilityRole,
28
28
  }) => {
29
29
  const [visible, setVisible] = useState(false);
30
- const timeoutRef = useRef<NodeJS.Timeout | null>(null);
30
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
31
31
  const anchorRef = useRef<HTMLDivElement>(null);
32
32
 
33
33
  // Generate unique ID for tooltip
@@ -105,9 +105,9 @@ const Tooltip: React.FC<TooltipProps> = ({
105
105
  return (
106
106
  <>
107
107
  <div
108
- ref={anchorRef}
109
108
  {...containerProps}
110
109
  {...ariaProps}
110
+ ref={anchorRef}
111
111
  id={triggerId}
112
112
  onMouseEnter={handleMouseEnter}
113
113
  onMouseLeave={handleMouseLeave}
@@ -123,7 +123,7 @@ const Tooltip: React.FC<TooltipProps> = ({
123
123
 
124
124
  <PositionedPortal
125
125
  open={visible}
126
- anchor={anchorRef}
126
+ anchor={anchorRef as React.RefObject<HTMLElement>}
127
127
  placement={placement}
128
128
  offset={8}
129
129
  zIndex={1000}
@@ -20,7 +20,7 @@ export const videoStyles = defineStyle('Video', (theme: Theme) => ({
20
20
  container: (_props: VideoDynamicProps) => ({
21
21
  position: 'relative' as const,
22
22
  overflow: 'hidden' as const,
23
- backgroundColor: theme.colors['black'],
23
+ backgroundColor: theme.colors.pallet.gray[900],
24
24
  }),
25
25
 
26
26
  video: (_props: VideoDynamicProps) => ({
@@ -37,7 +37,7 @@ export const videoStyles = defineStyle('Video', (theme: Theme) => ({
37
37
  display: 'flex' as const,
38
38
  alignItems: 'center' as const,
39
39
  justifyContent: 'center' as const,
40
- backgroundColor: theme.colors['gray.300'],
41
- color: theme.colors['gray.600'],
40
+ backgroundColor: theme.colors.pallet.gray[300],
41
+ color: theme.colors.pallet.gray[600],
42
42
  }),
43
43
  }));
@@ -45,7 +45,7 @@ const Video: React.FC<VideoProps> = ({
45
45
  const videoRef = useRef<HTMLVideoElement>(null);
46
46
 
47
47
  const containerProps = getWebProps([videoStyles.container, style as any]);
48
- const videoProps = getWebProps([videoStyles.video]);
48
+ const videoProps = getWebProps([videoStyles.video as any]);
49
49
 
50
50
  const videoSource = typeof source === 'string'
51
51
  ? source
@@ -60,8 +60,8 @@ export const viewStyles = defineStyle('View', (theme: Theme) => ({
60
60
  },
61
61
  border: {
62
62
  none: { borderWidth: 0 },
63
- thin: { borderWidth: 1, borderStyle: 'solid' as const, borderColor: theme.colors['gray.300'] },
64
- thick: { borderWidth: 2, borderStyle: 'solid' as const, borderColor: theme.colors['gray.300'] },
63
+ thin: { borderWidth: 1, borderStyle: 'solid' as const, borderColor: theme.colors.pallet.gray[300] },
64
+ thick: { borderWidth: 2, borderStyle: 'solid' as const, borderColor: theme.colors.pallet.gray[300] },
65
65
  },
66
66
  // $iterator expands for each view size
67
67
  gap: {
@@ -1,6 +1,5 @@
1
- import React, { forwardRef, useMemo } from 'react';
1
+ import React, { forwardRef } from 'react';
2
2
  import { getWebProps } from 'react-native-unistyles/web';
3
- import { useResponsiveStyle } from '@idealyst/theme';
4
3
  import { ViewProps } from './types';
5
4
  import { viewStyles } from './View.styles';
6
5
  import useMergeRefs from '../hooks/useMergeRefs';
@@ -22,12 +21,12 @@ const View = forwardRef<HTMLDivElement, ViewProps>(({
22
21
  margin,
23
22
  marginVertical,
24
23
  marginHorizontal,
25
- // Override props
26
- backgroundColor,
27
- borderRadius,
28
- borderWidth,
29
- borderColor,
30
- scrollable, // accepted but no-op on web - layout handles scrolling
24
+ // Override props (accepted but handled via style prop on web)
25
+ backgroundColor: _backgroundColor,
26
+ borderRadius: _borderRadius,
27
+ borderWidth: _borderWidth,
28
+ borderColor: _borderColor,
29
+ scrollable,
31
30
  style,
32
31
  testID,
33
32
  id,
@@ -48,9 +47,96 @@ const View = forwardRef<HTMLDivElement, ViewProps>(({
48
47
  // Call style as function to get theme-reactive styles
49
48
  /** @ts-ignore */
50
49
  const webProps = getWebProps((viewStyles.view as any)({}));
51
-
50
+
52
51
  const mergedRef = useMergeRefs(ref, webProps.ref);
53
52
 
53
+ // When scrollable, render a wrapper + content structure
54
+ // Wrapper: sizing and margin (positioning in parent layout)
55
+ // Content: absolutely positioned with overflow:auto, visual styles (padding, background, border)
56
+ if (scrollable) {
57
+ // Extract layout/sizing styles for the wrapper, visual styles go to content
58
+ const styleObj = (style as React.CSSProperties) || {};
59
+ const {
60
+ // Sizing - goes to wrapper
61
+ width,
62
+ height,
63
+ minWidth,
64
+ minHeight,
65
+ maxWidth,
66
+ maxHeight,
67
+ // Flex properties - goes to wrapper
68
+ flex,
69
+ flexGrow,
70
+ flexShrink,
71
+ flexBasis,
72
+ alignSelf,
73
+ // Margin - goes to wrapper (positioning in parent)
74
+ margin,
75
+ marginTop,
76
+ marginRight,
77
+ marginBottom,
78
+ marginLeft,
79
+ marginBlock,
80
+ marginInline,
81
+ // Everything else (padding, background, border, etc.) - goes to content
82
+ ...contentStyles
83
+ } = styleObj;
84
+
85
+ const wrapperStyles: React.CSSProperties = {
86
+ display: 'flex',
87
+ flexDirection: 'column',
88
+ position: 'relative',
89
+ boxSizing: 'border-box',
90
+ // If no explicit sizing, flex-grow to fill remaining space
91
+ ...(width === undefined && height === undefined && flex === undefined && { flex: 1 }),
92
+ // Apply sizing
93
+ width,
94
+ height,
95
+ minWidth,
96
+ minHeight: minHeight ?? 0, // Important for flex children to allow shrinking
97
+ maxWidth,
98
+ maxHeight,
99
+ // Apply flex properties
100
+ flex,
101
+ flexGrow,
102
+ flexShrink,
103
+ flexBasis,
104
+ alignSelf,
105
+ // Apply margin
106
+ margin,
107
+ marginTop,
108
+ marginRight,
109
+ marginBottom,
110
+ marginLeft,
111
+ marginBlock,
112
+ marginInline,
113
+ };
114
+
115
+ return (
116
+ <div style={wrapperStyles}>
117
+ <div
118
+ {...webProps}
119
+ style={{
120
+ // Content is block (not flex) for natural scrolling behavior
121
+ display: 'block',
122
+ // Absolutely positioned to fill wrapper
123
+ position: 'absolute',
124
+ inset: 0,
125
+ overflow: 'auto',
126
+ boxSizing: 'border-box',
127
+ // Apply visual styles (padding, background, border, etc.)
128
+ ...contentStyles,
129
+ }}
130
+ ref={mergedRef}
131
+ id={id}
132
+ data-testid={testID}
133
+ >
134
+ {children}
135
+ </div>
136
+ </div>
137
+ );
138
+ }
139
+
54
140
  return (
55
141
  <div
56
142
  style={style as any}
package/src/View/types.ts CHANGED
@@ -87,7 +87,11 @@ export interface ViewProps extends ContainerStyleProps {
87
87
  style?: ViewStyleProp;
88
88
 
89
89
  /**
90
- * Enable scrollable content (uses ScrollView on native, overflow:auto on web)
90
+ * Enable scrollable content.
91
+ * - Native: Wraps children in a ScrollView
92
+ * - Web: Renders a wrapper + content structure where the wrapper fills available
93
+ * space (or uses explicit dimensions) and content is absolutely positioned with
94
+ * overflow:auto. Sizing/margin styles go to wrapper, visual styles to content.
91
95
  */
92
96
  scrollable?: boolean;
93
97
 
@@ -131,6 +131,40 @@ export const ViewExamples = () => {
131
131
  </View>
132
132
  </View>
133
133
  </View>
134
+
135
+ {/* Scrollable Example */}
136
+ <View gap="md">
137
+ <Text typography="subtitle1">Scrollable View</Text>
138
+ <Text typography="body2" color="secondary">
139
+ Fixed 200x200px container with scrollable content
140
+ </Text>
141
+ <View
142
+ scrollable
143
+ background="secondary"
144
+ padding="md"
145
+ radius="md"
146
+ border="thin"
147
+ style={{ width: 200, height: 200 }}
148
+ >
149
+ <View>
150
+ <Text typography="body2">Line 1: Scroll down to see more content</Text>
151
+ <Text typography="body2">Line 2</Text>
152
+ <Text typography="body2">Line 3</Text>
153
+ <Text typography="body2">Line 4</Text>
154
+ <Text typography="body2">Line 5</Text>
155
+ <Text typography="body2">Line 6</Text>
156
+ <Text typography="body2">Line 7</Text>
157
+ <Text typography="body2">Line 8</Text>
158
+ <Text typography="body2">Line 9</Text>
159
+ <Text typography="body2">Line 10</Text>
160
+ <Text typography="body2">Line 11</Text>
161
+ <Text typography="body2">Line 12</Text>
162
+ <Text typography="body2">Line 13</Text>
163
+ <Text typography="body2">Line 14</Text>
164
+ <Text typography="body2">Line 15: End of scrollable content</Text>
165
+ </View>
166
+ </View>
167
+ </View>
134
168
  </View>
135
169
  </Screen>
136
170
  );
@@ -49,13 +49,6 @@ export {
49
49
 
50
50
  // Internal API (for component stylesheets)
51
51
  export { getExtension, getReplacement } from './extendComponent';
52
- export {
53
- withExtension,
54
- withSimpleExtension,
55
- normalizeStyleFn,
56
- normalizeSimpleStyleFn,
57
- applyExtensions,
58
- } from './applyExtension';
59
52
 
60
53
  // Types
61
54
  export type {
@@ -1,14 +1,20 @@
1
1
  /**
2
- * Userful when workign with getWebProps and forwarding refs
3
- * @param refs
4
- * @returns
2
+ * Useful when working with getWebProps and forwarding refs.
3
+ * Accepts various ref types including callback refs and ref objects.
4
+ * Uses a permissive type to handle refs from different React type versions in monorepos.
5
+ * @param refs - Array of refs to merge (can include undefined values)
6
+ * @returns A ref callback that updates all provided refs
5
7
  */
6
- export default function useMergeRefs<T>(...refs: React.Ref<T>[]): React.RefCallback<T> {
7
- return (value: T) => {
8
+ export default function useMergeRefs<T>(
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ ...refs: any[]
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ ): any {
13
+ return (value: T | null) => {
8
14
  refs.forEach((ref) => {
9
15
  if (typeof ref === 'function') {
10
16
  ref(value);
11
- } else if (ref && 'current' in ref) {
17
+ } else if (ref && typeof ref === 'object' && 'current' in ref) {
12
18
  ref.current = value;
13
19
  }
14
20
  });
@@ -35,6 +35,10 @@ export const MENU_KEYS = {
35
35
  navigateVertical: ['ArrowUp', 'ArrowDown'] as const,
36
36
  /** Keys for navigating horizontal menus/menubars */
37
37
  navigateHorizontal: ['ArrowLeft', 'ArrowRight'] as const,
38
+ /** Keys for moving to next item (down or right) */
39
+ next: ['ArrowDown', 'ArrowRight'] as const,
40
+ /** Keys for moving to previous item (up or left) */
41
+ prev: ['ArrowUp', 'ArrowLeft'] as const,
38
42
  /** Keys for selecting an item */
39
43
  select: ['Enter', ' '] as const,
40
44
  /** Keys for jumping to first item */
@@ -66,7 +66,11 @@ export type AriaRole =
66
66
  | 'none'
67
67
  | 'presentation'
68
68
  | 'heading'
69
- | 'group';
69
+ | 'group'
70
+ // Additional composite/group roles
71
+ | 'radiogroup'
72
+ // React Native specific roles
73
+ | 'adjustable';
70
74
 
71
75
  /**
72
76
  * Base accessibility props shared across all components.
@@ -77,7 +77,7 @@ export function useAnnounce(options: UseAnnounceOptions = {}): UseAnnounceReturn
77
77
 
78
78
  const politeRegionRef = useRef<HTMLDivElement | null>(null);
79
79
  const assertiveRegionRef = useRef<HTMLDivElement | null>(null);
80
- const clearTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
80
+ const clearTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
81
81
 
82
82
  /**
83
83
  * Create a live region element.
@@ -103,7 +103,7 @@ export function useKeyboardNavigation({
103
103
  }: UseKeyboardNavigationOptions): UseKeyboardNavigationReturn {
104
104
  // Buffer for type-ahead search
105
105
  const searchBuffer = useRef('');
106
- const searchTimeout = useRef<ReturnType<typeof setTimeout>>();
106
+ const searchTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
107
107
 
108
108
  /**
109
109
  * Focus a specific item by index.
@@ -1,6 +1,3 @@
1
- // Size variant builder
2
- export { buildSizeVariants } from './buildSizeVariants';
3
-
4
1
  // View/container style variant builders
5
2
  export {
6
3
  buildGapVariants,
@@ -20,6 +20,8 @@ export interface BaseProps {
20
20
  export interface GapStyleProps {
21
21
  /** Gap between children (uses theme.sizes.view[size].spacing) */
22
22
  gap?: ViewStyleSize;
23
+ /** Alias for gap - spacing between children */
24
+ spacing?: ViewStyleSize;
23
25
  }
24
26
 
25
27
  /**