@utilitywarehouse/hearth-react-native 0.16.2 → 0.18.0

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 (75) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +14 -14
  3. package/CHANGELOG.md +174 -0
  4. package/build/components/BodyText/BodyText.js +2 -2
  5. package/build/components/Card/CardAction/CardActionRoot.js +12 -2
  6. package/build/components/Card/CardActions.context.d.ts +6 -0
  7. package/build/components/Card/CardActions.context.js +5 -0
  8. package/build/components/Card/CardActions.d.ts +7 -0
  9. package/build/components/Card/CardActions.js +29 -0
  10. package/build/components/Card/CardRoot.js +16 -104
  11. package/build/components/Card/helpers.d.ts +8 -0
  12. package/build/components/Card/helpers.js +146 -0
  13. package/build/components/Card/index.d.ts +2 -0
  14. package/build/components/Card/index.js +2 -0
  15. package/build/components/ExpandableCard/ExpandableCardGroup.d.ts +1 -1
  16. package/build/components/ExpandableCard/ExpandableCardGroup.js +2 -2
  17. package/build/components/ExpandableCard/ExpandableCardGroup.props.d.ts +4 -0
  18. package/build/components/IconButton/IconButton.props.d.ts +19 -0
  19. package/build/components/IconButton/IconButtonRoot.d.ts +1 -1
  20. package/build/components/IconButton/IconButtonRoot.js +43 -2
  21. package/build/components/Input/Input.js +4 -3
  22. package/build/components/Input/Input.props.d.ts +9 -0
  23. package/build/components/List/List.context.d.ts +4 -2
  24. package/build/components/List/List.context.js +0 -2
  25. package/build/components/List/List.d.ts +1 -1
  26. package/build/components/List/List.js +25 -38
  27. package/build/components/List/List.props.d.ts +1 -0
  28. package/build/components/List/ListAction/ListAction.js +24 -7
  29. package/build/components/List/ListAction/ListAction.props.d.ts +1 -0
  30. package/build/components/List/ListItem/ListItemHelperText.d.ts +1 -1
  31. package/build/components/List/ListItem/ListItemHelperText.js +2 -2
  32. package/build/components/List/ListItem/ListItemRoot.js +12 -4
  33. package/build/utils/isThemedImageProps.d.ts +1 -1
  34. package/package.json +2 -2
  35. package/src/components/BodyText/BodyText.tsx +2 -2
  36. package/src/components/Card/Card.docs.mdx +224 -66
  37. package/src/components/Card/Card.stories.tsx +29 -25
  38. package/src/components/Card/CardAction/CardAction.stories.tsx +239 -93
  39. package/src/components/Card/CardAction/CardActionRoot.tsx +15 -2
  40. package/src/components/Card/CardActions.context.ts +12 -0
  41. package/src/components/Card/CardActions.tsx +40 -0
  42. package/src/components/Card/CardRoot.tsx +27 -132
  43. package/src/components/Card/helpers.tsx +195 -0
  44. package/src/components/Card/index.ts +2 -0
  45. package/src/components/ExpandableCard/ExpandableCard.figma.tsx +33 -38
  46. package/src/components/ExpandableCard/ExpandableCardGroup.figma.tsx +34 -17
  47. package/src/components/ExpandableCard/ExpandableCardGroup.props.ts +5 -0
  48. package/src/components/ExpandableCard/ExpandableCardGroup.tsx +2 -0
  49. package/src/components/HighlightBanner/HighlightBanner.figma.tsx +46 -0
  50. package/src/components/IconButton/IconButton.docs.mdx +91 -9
  51. package/src/components/IconButton/IconButton.figma.tsx +20 -30
  52. package/src/components/IconButton/IconButton.props.ts +19 -0
  53. package/src/components/IconButton/IconButton.stories.tsx +56 -0
  54. package/src/components/IconButton/IconButtonRoot.tsx +54 -1
  55. package/src/components/IconContainer/IconContainer.figma.tsx +7 -13
  56. package/src/components/IndicatorIconButton/IndicatorIconButton.figma.tsx +16 -0
  57. package/src/components/Input/Input.docs.mdx +55 -15
  58. package/src/components/Input/Input.figma.tsx +106 -40
  59. package/src/components/Input/Input.props.ts +9 -0
  60. package/src/components/Input/Input.tsx +21 -0
  61. package/src/components/Link/Link.figma.tsx +31 -38
  62. package/src/components/List/List.context.ts +2 -4
  63. package/src/components/List/List.docs.mdx +10 -5
  64. package/src/components/List/List.figma.tsx +42 -28
  65. package/src/components/List/List.props.ts +1 -0
  66. package/src/components/List/List.stories.tsx +43 -0
  67. package/src/components/List/List.tsx +38 -51
  68. package/src/components/List/ListAction/ListAction.figma.tsx +5 -13
  69. package/src/components/List/ListAction/ListAction.props.ts +1 -0
  70. package/src/components/List/ListAction/ListAction.tsx +40 -10
  71. package/src/components/List/ListItem/ListItem.figma.tsx +43 -27
  72. package/src/components/List/ListItem/ListItemHelperText.tsx +2 -2
  73. package/src/components/List/ListItem/ListItemRoot.tsx +15 -4
  74. package/src/utils/isThemedImageProps.ts +1 -1
  75. package/src/components/InlineLink/InlineLink.figma.tsx +0 -33
@@ -1,4 +1,4 @@
1
1
 
2
- > @utilitywarehouse/hearth-react-native@0.16.2 build /home/runner/work/hearth/hearth/packages/react-native
2
+ > @utilitywarehouse/hearth-react-native@0.18.0 build /home/runner/work/hearth/hearth/packages/react-native
3
3
  > tsc
4
4
 
@@ -1,5 +1,5 @@
1
1
 
2
- > @utilitywarehouse/hearth-react-native@0.16.2 lint /home/runner/work/hearth/hearth/packages/react-native
2
+ > @utilitywarehouse/hearth-react-native@0.18.0 lint /home/runner/work/hearth/hearth/packages/react-native
3
3
  > TIMING=1 eslint .
4
4
 
5
5
 
@@ -28,7 +28,7 @@
28
28
  52:6 warning React Hook useCallback has a missing dependency: 'containerHeight'. Either include it or remove the dependency array. Outer scope values like 'styles' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
29
29
 
30
30
  /home/runner/work/hearth/hearth/packages/react-native/src/components/Input/Input.tsx
31
- 75:8 warning React Hook useEffect has a missing dependency: 'formFieldContext'. Either include it or remove the dependency array react-hooks/exhaustive-deps
31
+ 78:8 warning React Hook useEffect has a missing dependency: 'formFieldContext'. Either include it or remove the dependency array react-hooks/exhaustive-deps
32
32
 
33
33
  /home/runner/work/hearth/hearth/packages/react-native/src/components/Modal/Modal.tsx
34
34
  72:6 warning React Hook useCallback has an unnecessary dependency: 'Platform.OS'. Either exclude it or remove the dependency array. Outer scope values like 'Platform.OS' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
@@ -55,15 +55,15 @@
55
55
 
56
56
  ✖ 24 problems (0 errors, 24 warnings)
57
57
 
58
- Rule | Time (ms) | Relative
59
- :-----------------------------------------|----------:|--------:
60
- @typescript-eslint/no-unused-vars | 1369.850 | 61.9%
61
- react-hooks/rules-of-hooks | 90.157 | 4.1%
62
- react-hooks/exhaustive-deps | 78.401 | 3.5%
63
- no-global-assign | 65.908 | 3.0%
64
- no-loss-of-precision | 42.121 | 1.9%
65
- no-unexpected-multiline | 40.780 | 1.8%
66
- @typescript-eslint/ban-ts-comment | 35.389 | 1.6%
67
- no-misleading-character-class | 33.623 | 1.5%
68
- @typescript-eslint/triple-slash-reference | 22.898 | 1.0%
69
- prefer-const | 21.548 | 1.0%
58
+ Rule | Time (ms) | Relative
59
+ :---------------------------------|----------:|--------:
60
+ @typescript-eslint/no-unused-vars | 1538.884 | 63.7%
61
+ no-global-assign | 88.035 | 3.6%
62
+ react-hooks/exhaustive-deps | 86.501 | 3.6%
63
+ react-hooks/rules-of-hooks | 67.331 | 2.8%
64
+ no-misleading-character-class | 48.025 | 2.0%
65
+ no-unexpected-multiline | 45.795 | 1.9%
66
+ no-loss-of-precision | 34.916 | 1.4%
67
+ @typescript-eslint/ban-ts-comment | 33.872 | 1.4%
68
+ no-regex-spaces | 31.659 | 1.3%
69
+ no-fallthrough | 31.350 | 1.3%
package/CHANGELOG.md CHANGED
@@ -1,5 +1,179 @@
1
1
  # @utilitywarehouse/hearth-react-native
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#869](https://github.com/utilitywarehouse/hearth/pull/869) [`89231c8`](https://github.com/utilitywarehouse/hearth/commit/89231c818dfd694f53b50f4cc961a38d0a50999e) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add color override props to `IconButton` for service-specific branding
8
+
9
+ The `IconButton` component now supports custom color overrides through three new optional props: `backgroundColor`, `activeBackgroundColor`, and `shadowColor`. These props enable service-specific branding for use cases like service buttons (Electricity, Broadband, Mobile, Insurance, Cashback Card).
10
+
11
+ ⚠️ **Important**: These props should be used sparingly and only for specific use cases where brand-specific colors are required. For most use cases, continue using the standard `colorScheme` and `variant` props to maintain design system consistency.
12
+
13
+ **Components affected**:
14
+ - `IconButton`
15
+
16
+ **Developer changes**:
17
+
18
+ You can now customize `IconButton` colors for service-specific branding:
19
+
20
+ ```tsx
21
+ import { IconButton } from '@utilitywarehouse/hearth-react-native';
22
+ import { ElectricityMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
23
+
24
+ <IconButton
25
+ icon={ElectricityMediumIcon}
26
+ backgroundColor="energyBlue200"
27
+ activeBackgroundColor="energyBlue300"
28
+ shadowColor="energyBlue300"
29
+ variant="emphasis"
30
+ onPress={handlePress}
31
+ />;
32
+ ```
33
+
34
+ The new props are:
35
+ - `backgroundColor` - Sets the base background color, overriding the color scheme's default
36
+ - `activeBackgroundColor` - Sets the background color when pressed or in an active state
37
+ - `shadowColor` - Sets the shadow/elevation color
38
+
39
+ These overrides work alongside the existing `variant` and `colorScheme` props, allowing you to maintain structural styling while customizing colors for specific branding requirements.
40
+
41
+ ### Patch Changes
42
+
43
+ - [#871](https://github.com/utilitywarehouse/hearth/pull/871) [`8984335`](https://github.com/utilitywarehouse/hearth/commit/89843355d268aaa184bea75784e6841568c4784f) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Fix style ordering in `BodyText` to ensure color props properly override custom styles
44
+
45
+ ## 0.17.0
46
+
47
+ ### Minor Changes
48
+
49
+ - [#867](https://github.com/utilitywarehouse/hearth/pull/867) [`9a15eb8`](https://github.com/utilitywarehouse/hearth/commit/9a15eb8c659aa541988da6f28f6c50261f3557f9) Thanks [@jordmccord](https://github.com/jordmccord)! - 💔 [BREAKING CHANGE]: Require `CardActions` wrapper for `CardAction` groups.
50
+
51
+ `Card` now only treats actions as such when they are wrapped in `CardActions`. This removes wrapper heuristics and makes action grouping explicit while keeping automatic content wrapping.
52
+
53
+ **Components affected**:
54
+ - `Card`
55
+ - `CardActions`
56
+ - `CardAction`
57
+
58
+ **Developer changes**:
59
+
60
+ Wrap all `CardAction` items in `CardActions`:
61
+
62
+ ```diff
63
+ - <Card>
64
+ - <CardAction heading="Action 1" onPress={() => {}} />
65
+ - <CardAction heading="Action 2" onPress={() => {}} />
66
+ - </Card>
67
+ + <Card>
68
+ + <CardActions>
69
+ + <CardAction heading="Action 1" onPress={() => {}} />
70
+ + <CardAction heading="Action 2" onPress={() => {}} />
71
+ + </CardActions>
72
+ + </Card>
73
+ ```
74
+
75
+ - [#860](https://github.com/utilitywarehouse/hearth/pull/860) [`ec44a9d`](https://github.com/utilitywarehouse/hearth/commit/ec44a9d3d7a2d95ab69b6e4c461104402d82659d) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `prefix` and `suffix` props to `Input` component
76
+
77
+ The `Input` component now supports `prefix` and `suffix` props, allowing you to display text or custom content before and after the input field. This is useful for adding units, currency symbols, or other contextual information.
78
+
79
+ **Components affected**:
80
+ - `Input`
81
+
82
+ **Developer changes**:
83
+
84
+ Use the `prefix` and `suffix` props to add content before or after the input:
85
+
86
+ ```tsx
87
+ <Input label="Amount" prefix="£" suffix="GBP" placeholder="0.00" />
88
+ ```
89
+
90
+ You can also pass custom React nodes:
91
+
92
+ ```tsx
93
+ <Input label="Email" prefix={<CustomIcon />} suffix={<BodyText>@example.com</BodyText>} />
94
+ ```
95
+
96
+ **Note**: The `prefix` and `suffix` props are not available on `password` and `search` input types, as these have specific UI patterns.
97
+
98
+ - [#862](https://github.com/utilitywarehouse/hearth/pull/862) [`654552e`](https://github.com/utilitywarehouse/hearth/commit/654552e33d56e4b2b2ba8fb783b7f9a7c57ba212) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `loading` prop to `ListAction` component
99
+
100
+ The `ListAction` component now supports a `loading` prop that displays a skeleton loading state while content is being fetched. This provides better user feedback during asynchronous operations.
101
+
102
+ **Components affected**:
103
+ - `ListAction`
104
+
105
+ **Developer changes**:
106
+
107
+ Use the `loading` prop to show a loading state:
108
+
109
+ ```tsx
110
+ <List>
111
+ <ListItem heading="Account details" />
112
+ <ListAction heading="View transactions" loading={isLoading} onPress={handlePress} />
113
+ </List>
114
+ ```
115
+
116
+ When `loading` is true, the action will display skeleton placeholders instead of the heading and icon.
117
+
118
+ - [#862](https://github.com/utilitywarehouse/hearth/pull/862) [`654552e`](https://github.com/utilitywarehouse/hearth/commit/654552e33d56e4b2b2ba8fb783b7f9a7c57ba212) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `invalidText` prop to `List` component
119
+
120
+ The `List` component now supports an `invalidText` prop for displaying validation error messages in the section header. This provides consistent validation feedback across list-based forms and grouped content.
121
+
122
+ **Components affected**:
123
+ - `List`
124
+
125
+ **Developer changes**:
126
+
127
+ Use the `invalidText` prop to display validation errors:
128
+
129
+ ```tsx
130
+ <List
131
+ heading="Payment methods"
132
+ helperText="Select at least one payment method"
133
+ invalidText="You must select a payment method"
134
+ >
135
+ <ListItem heading="Credit card" />
136
+ <ListItem heading="Direct debit" />
137
+ </List>
138
+ ```
139
+
140
+ ### Patch Changes
141
+
142
+ - [#845](https://github.com/utilitywarehouse/hearth/pull/845) [`9c034f9`](https://github.com/utilitywarehouse/hearth/commit/9c034f98f9d6aa4596a45296d02e01703ef1c762) Thanks [@jordmccord](https://github.com/jordmccord)! - 💅 [ENHANCEMENT]: Add `invalidText` prop to `ExpandableCardGroup`
143
+
144
+ The `ExpandableCardGroup` component now supports an `invalidText` prop that displays validation text below the helper text when the group is in an invalid state.
145
+
146
+ **Components affected**:
147
+ - `ExpandableCardGroup`
148
+
149
+ **Developer changes**:
150
+
151
+ No changes required. If you want to display validation text, you can now use the `invalidText` prop:
152
+
153
+ ```tsx
154
+ <ExpandableCardGroup
155
+ heading="Select an option"
156
+ helperText="Choose one of the options below"
157
+ invalidText="Please select at least one option"
158
+ >
159
+ {/* ExpandableCard components */}
160
+ </ExpandableCardGroup>
161
+ ```
162
+
163
+ - [#867](https://github.com/utilitywarehouse/hearth/pull/867) [`9a15eb8`](https://github.com/utilitywarehouse/hearth/commit/9a15eb8c659aa541988da6f28f6c50261f3557f9) Thanks [@jordmccord](https://github.com/jordmccord)! - 💅 [ENHANCEMENT]: Improve first-item border detection in `Card` and `List` components
164
+
165
+ The `Card` and `List` components now use a more reliable method to detect and style the first rendered `CardAction`, `ListItem`, or `ListAction`. This fixes edge cases where wrapper components that conditionally render `null` would previously interfere with first-item border removal.
166
+
167
+ **Components affected**:
168
+ - `Card` / `CardAction`
169
+ - `List` / `ListItem` / `ListAction`
170
+
171
+ **Developer changes**:
172
+
173
+ No changes required. The improvement is automatic and maintains the same visual behavior. Components that wrap card actions or list items will now work correctly even when some wrappers return `null` conditionally.
174
+
175
+ **Note**: The `useCardFirstActionContext` hook has been removed as it was an internal implementation detail.
176
+
3
177
  ## 0.16.2
4
178
 
5
179
  ### Patch Changes
@@ -18,7 +18,7 @@ const BodyText = ({ children, color, size = 'md', truncated, weight = 'regular',
18
18
  }
19
19
  : {}), style: [
20
20
  styles.text,
21
- styles.getColours(color, textDecorationColor),
21
+ props.style,
22
22
  {
23
23
  ...(textTransform && { textTransform }),
24
24
  ...(textAlign && { textAlign }),
@@ -27,7 +27,7 @@ const BodyText = ({ children, color, size = 'md', truncated, weight = 'regular',
27
27
  ...(userSelect && { userSelect }),
28
28
  ...(textAlignVertical && { textAlignVertical }),
29
29
  },
30
- props.style,
30
+ styles.getColours(color, textDecorationColor),
31
31
  ], children: children }));
32
32
  };
33
33
  BodyText.displayName = 'BodyText';
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { ChevronRightSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
3
- import { useMemo } from 'react';
3
+ import { useId, useLayoutEffect, useMemo } from 'react';
4
4
  import { Pressable, View } from 'react-native';
5
5
  import { StyleSheet } from 'react-native-unistyles';
6
6
  import { IconContainer } from '../../IconContainer';
7
7
  import { Skeleton } from '../../Skeleton';
8
8
  import { useCardContext } from '../Card.context';
9
+ import { useCardActionsContext } from '../CardActions.context';
9
10
  import { CardActionContext } from './CardAction.context';
10
11
  import CardActionContent from './CardActionContent';
11
12
  import CardActionHelperText from './CardActionHelperText';
@@ -23,7 +24,16 @@ const CardActionRoot = ({ heading, helperText, leadingContent, trailingContent,
23
24
  const testID = props.testID || 'card-action';
24
25
  const loadingTestID = isLoading ? `${testID}-loading` : testID;
25
26
  const { variant, hasOnlyActions } = useCardContext();
26
- const isFirst = props.isFirst;
27
+ const actionId = useId();
28
+ const actionsContext = useCardActionsContext();
29
+ useLayoutEffect(() => {
30
+ if (!actionsContext) {
31
+ return;
32
+ }
33
+ return actionsContext.registerAction(actionId);
34
+ }, [actionId, actionsContext]);
35
+ const isFirstFromContext = actionsContext?.firstActionId === actionId;
36
+ const isFirst = props.isFirst ?? isFirstFromContext;
27
37
  styles.useVariants({
28
38
  showPressed,
29
39
  active,
@@ -0,0 +1,6 @@
1
+ export interface CardActionsContextValue {
2
+ firstActionId?: string;
3
+ registerAction: (id: string) => () => void;
4
+ }
5
+ export declare const CardActionsContext: import("react").Context<CardActionsContextValue | null>;
6
+ export declare const useCardActionsContext: () => CardActionsContextValue | null;
@@ -0,0 +1,5 @@
1
+ import { createContext, useContext } from 'react';
2
+ export const CardActionsContext = createContext(null);
3
+ export const useCardActionsContext = () => {
4
+ return useContext(CardActionsContext);
5
+ };
@@ -0,0 +1,7 @@
1
+ import { PropsWithChildren } from 'react';
2
+ import { ViewProps } from 'react-native';
3
+ declare const CardActions: {
4
+ ({ children, style, ...props }: PropsWithChildren<ViewProps>): import("react/jsx-runtime").JSX.Element;
5
+ displayName: string;
6
+ };
7
+ export default CardActions;
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useRef, useState } from 'react';
3
+ import { View } from 'react-native';
4
+ import { StyleSheet } from 'react-native-unistyles';
5
+ import { CardActionsContext } from './CardActions.context';
6
+ const CardActions = ({ children, style, ...props }) => {
7
+ const orderRef = useRef([]);
8
+ const [firstActionId, setFirstActionId] = useState(undefined);
9
+ const registerAction = useCallback((id) => {
10
+ if (!orderRef.current.includes(id)) {
11
+ orderRef.current.push(id);
12
+ }
13
+ const nextFirst = orderRef.current[0];
14
+ setFirstActionId(prev => (prev === nextFirst ? prev : nextFirst));
15
+ return () => {
16
+ orderRef.current = orderRef.current.filter(currentId => currentId !== id);
17
+ const nextFirst = orderRef.current[0];
18
+ setFirstActionId(prev => (prev === nextFirst ? prev : nextFirst));
19
+ };
20
+ }, []);
21
+ return (_jsx(CardActionsContext.Provider, { value: { firstActionId, registerAction }, children: _jsx(View, { ...props, style: [styles.container, style], children: children }) }));
22
+ };
23
+ CardActions.displayName = 'CardActions';
24
+ const styles = StyleSheet.create({
25
+ container: {
26
+ width: '100%',
27
+ },
28
+ });
29
+ export default CardActions;
@@ -1,93 +1,17 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useMemo } from 'react';
2
+ import { useMemo } from 'react';
3
3
  import { Pressable } from 'react-native';
4
4
  import { StyleSheet } from 'react-native-unistyles';
5
5
  import { useStyleProps } from '../../hooks';
6
6
  import { CardContext } from './Card.context';
7
+ import CardActions from './CardActions';
7
8
  import CardContent from './CardContent';
8
- // Helper to check if children contain specific component types
9
- const checkForComponentType = (children, displayName) => {
10
- return React.Children.toArray(children).some(child => {
11
- if (React.isValidElement(child)) {
12
- // @ts-expect-error - type
13
- if (child.type.displayName === displayName) {
14
- return true;
15
- }
16
- const childProps = child.props;
17
- if (childProps.children) {
18
- return checkForComponentType(childProps.children, displayName);
19
- }
20
- }
21
- return false;
22
- });
23
- };
24
- // Helper to filter out specific component types from children
25
- const filterChildren = (children, excludeDisplayName) => {
26
- return React.Children.map(children, child => {
27
- if (React.isValidElement(child)) {
28
- // @ts-expect-error - type
29
- if (child.type.displayName === excludeDisplayName) {
30
- return null;
31
- }
32
- const childProps = child.props;
33
- if (childProps.children) {
34
- return React.cloneElement(child, {
35
- ...childProps,
36
- children: filterChildren(childProps.children, excludeDisplayName),
37
- });
38
- }
39
- }
40
- return child;
41
- });
42
- };
43
- // Helper to extract specific component types from children
44
- const extractChildren = (children, includeDisplayName, markFirst = false) => {
45
- let isFirstFound = false;
46
- return React.Children.map(children, child => {
47
- if (React.isValidElement(child)) {
48
- // @ts-expect-error - type
49
- if (child.type.displayName === includeDisplayName) {
50
- const isFirst = markFirst && !isFirstFound;
51
- if (isFirst) {
52
- isFirstFound = true;
53
- }
54
- return markFirst
55
- ? React.cloneElement(child, { ...(child.props || {}), isFirst })
56
- : child;
57
- }
58
- const childProps = child.props;
59
- if (childProps.children) {
60
- return extractChildren(childProps.children, includeDisplayName, markFirst);
61
- }
62
- }
63
- return null;
64
- });
65
- };
66
- // Helper that recursively collects onPress or other defined handlers from descendants
67
- const collectChildActionHandlers = (children) => React.Children.toArray(children).reduce((handlers, child) => {
68
- if (React.isValidElement(child)) {
69
- const childProps = child.props;
70
- // @ts-expect-error - type
71
- if (child.type.displayName === 'CardPressHandler') {
72
- const actionChildren = React.Children.toArray(childProps.children);
73
- const handlerToInherit = childProps['handlerToInherit'] || 'onPress';
74
- const firstChild = actionChildren[0];
75
- if (React.isValidElement(firstChild) &&
76
- typeof firstChild.props[handlerToInherit] === 'function') {
77
- handlers.push(firstChild.props[handlerToInherit]);
78
- }
79
- }
80
- if (childProps.children) {
81
- handlers.push(...collectChildActionHandlers(childProps.children));
82
- }
83
- }
84
- return handlers;
85
- }, []);
9
+ import { checkForComponentType, collectChildActionHandlers, extractCardActions, filterChildren, hasOnlyPotentialActions, } from './helpers';
86
10
  const Card = ({ children, variant = 'subtle', colorScheme = 'neutralStrong', shadowColor, noPadding = false, style, states, space, disabled = false, onPress, ...rest }) => {
87
11
  const { active } = states || { active: false };
88
12
  const childActionHandlers = collectChildActionHandlers(children);
89
- const hasActions = checkForComponentType(children, 'CardAction');
90
- const hasContent = checkForComponentType(children, 'CardContent');
13
+ const hasActions = checkForComponentType(children, CardActions);
14
+ const hasContent = checkForComponentType(children, CardContent);
91
15
  // Extract style props using our custom hook
92
16
  const { computedStyles, remainingProps } = useStyleProps(rest);
93
17
  const handlePress = (e) => {
@@ -98,14 +22,9 @@ const Card = ({ children, variant = 'subtle', colorScheme = 'neutralStrong', sha
98
22
  };
99
23
  const inheritChildAction = childActionHandlers.length > 0;
100
24
  const showPressed = inheritChildAction || !!onPress;
101
- const filteredChildren = !hasContent && hasActions ? filterChildren(children, 'CardAction') : null;
102
- // Check if there's any content besides CardActions
103
- const hasOnlyActions = hasActions &&
104
- !hasContent &&
105
- React.Children.toArray(filteredChildren).filter(child => child != null).length === 0;
106
- const filteredCardActions = !hasContent && hasActions
107
- ? extractChildren(children, 'CardAction', hasOnlyActions)
108
- : null;
25
+ // Check if all children are action groups (CardActions)
26
+ const potentiallyOnlyActions = hasOnlyPotentialActions(children, CardActions);
27
+ const hasOnlyActions = potentiallyOnlyActions && !hasContent;
109
28
  const context = useMemo(() => ({
110
29
  pressed: showPressed && active,
111
30
  noPadding,
@@ -118,29 +37,22 @@ const Card = ({ children, variant = 'subtle', colorScheme = 'neutralStrong', sha
118
37
  styles.useVariants({
119
38
  variant,
120
39
  colorScheme,
121
- noPadding: noPadding || hasActions || hasContent,
40
+ noPadding: noPadding || hasActions || hasContent || hasOnlyActions,
122
41
  active,
123
42
  showPressed,
124
43
  disabled,
125
- space: hasActions || hasContent ? 'none' : space,
44
+ space: hasActions || hasContent || hasOnlyActions ? 'none' : space,
126
45
  shadowColor,
127
46
  });
128
47
  const renderChildren = () => {
129
- // Default: render children as-is
130
- if (hasContent || !hasActions) {
48
+ // Explicit CardContent used - render as-is or Card has only actions (or potential action wrappers) - render children directly
49
+ if (hasContent || hasOnlyActions || !hasActions) {
131
50
  return children;
132
51
  }
133
- // Card has actions but no explicit CardContent
134
- if (hasOnlyActions) {
135
- // Only CardActions, no other content - render actions directly
136
- return filteredCardActions;
137
- }
138
- if (filteredChildren) {
139
- // Has both actions and other content - wrap content and render actions below
140
- return (_jsxs(_Fragment, { children: [_jsx(CardContent, { children: filteredChildren }), filteredCardActions] }));
141
- }
142
- // Fallback
143
- return children;
52
+ // Has both actions and other content - wrap non-action content and render actions separately
53
+ const filteredNonActionChildren = filterChildren(children, CardActions);
54
+ const cardActions = extractCardActions(children, CardActions);
55
+ return (_jsxs(_Fragment, { children: [_jsx(CardContent, { children: filteredNonActionChildren }), cardActions] }));
144
56
  };
145
57
  return (_jsx(CardContext.Provider, { value: context, children: _jsx(Pressable, { ...remainingProps, disabled: disabled, style: [styles.card, computedStyles, style], onPress: handlePress, accessible: showPressed, importantForAccessibility: showPressed ? 'yes' : 'no', children: renderChildren() }) }));
146
58
  };
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { GestureResponderEvent } from 'react-native';
3
+ export declare const checkForComponentType: (children: React.ReactNode, componentType: React.ComponentType<any>) => boolean;
4
+ export declare const hasContentInChildren: (child: React.ReactNode, actionType: React.ComponentType<any>) => boolean;
5
+ export declare const hasOnlyPotentialActions: (children: React.ReactNode, actionType: React.ComponentType<any>) => boolean;
6
+ export declare const filterChildren: (children: React.ReactNode, excludeComponentType: React.ComponentType<any>) => React.ReactNode;
7
+ export declare const extractCardActions: (children: React.ReactNode, actionType: React.ComponentType<any>) => React.ReactNode;
8
+ export declare const collectChildActionHandlers: (children: React.ReactNode) => Array<(e: GestureResponderEvent) => void>;
@@ -0,0 +1,146 @@
1
+ import React from 'react';
2
+ const getInheritableHandler = (child, handlerToInherit) => {
3
+ if (!React.isValidElement(child)) {
4
+ return null;
5
+ }
6
+ const childProps = child.props;
7
+ const isDisabled = !!childProps.disabled || !!childProps.loading;
8
+ if (isDisabled || typeof childProps[handlerToInherit] !== 'function') {
9
+ return null;
10
+ }
11
+ return childProps[handlerToInherit];
12
+ };
13
+ // Helper to check if children contain specific component types
14
+ export const checkForComponentType = (children, componentType) => {
15
+ return React.Children.toArray(children).some(child => {
16
+ if (React.isValidElement(child)) {
17
+ if (child.type === componentType) {
18
+ return true;
19
+ }
20
+ const childProps = child.props;
21
+ if (childProps.children) {
22
+ return checkForComponentType(childProps.children, componentType);
23
+ }
24
+ }
25
+ return false;
26
+ });
27
+ };
28
+ // Check if a component has "content" in its children (not just actions)
29
+ export const hasContentInChildren = (child, actionType) => {
30
+ if (!React.isValidElement(child)) {
31
+ return false;
32
+ }
33
+ const childProps = child.props;
34
+ if (!childProps?.children) {
35
+ return false;
36
+ }
37
+ // Check if children contain anything other than the action type
38
+ const childrenArray = React.Children.toArray(childProps.children);
39
+ return childrenArray.some(c => {
40
+ if (!React.isValidElement(c)) {
41
+ // Text, numbers, etc - this is content
42
+ return c != null;
43
+ }
44
+ return c.type !== actionType;
45
+ });
46
+ };
47
+ // Check if all children are CardActions groups
48
+ export const hasOnlyPotentialActions = (children, actionType) => {
49
+ const childArray = React.Children.toArray(children);
50
+ if (childArray.length === 0) {
51
+ return false;
52
+ }
53
+ let hasActionCandidate = false;
54
+ for (const child of childArray) {
55
+ if (!React.isValidElement(child)) {
56
+ if (child != null) {
57
+ return false;
58
+ }
59
+ continue;
60
+ }
61
+ if (child.type === actionType) {
62
+ hasActionCandidate = true;
63
+ continue;
64
+ }
65
+ if (checkForComponentType(child, actionType)) {
66
+ hasActionCandidate = true;
67
+ continue;
68
+ }
69
+ if (typeof child.type === 'string') {
70
+ return false;
71
+ }
72
+ if (hasContentInChildren(child, actionType)) {
73
+ return false;
74
+ }
75
+ return false;
76
+ }
77
+ return hasActionCandidate || checkForComponentType(children, actionType);
78
+ };
79
+ // Helper to filter out specific component types from children
80
+ export const filterChildren = (children, excludeComponentType) => {
81
+ return React.Children.map(children, child => {
82
+ if (React.isValidElement(child)) {
83
+ if (child.type === excludeComponentType) {
84
+ return null;
85
+ }
86
+ // Check if this child contains the excludeComponentType
87
+ if (checkForComponentType(child, excludeComponentType)) {
88
+ return null; // This child or its descendants contain the action
89
+ }
90
+ const childProps = child.props;
91
+ if (childProps.children) {
92
+ const filteredChildren = filterChildren(childProps.children, excludeComponentType);
93
+ // Only preserve wrapper if it has non-null children
94
+ const hasContent = React.Children.toArray(filteredChildren).some(c => c != null);
95
+ if (!hasContent) {
96
+ return null;
97
+ }
98
+ return React.cloneElement(child, {
99
+ ...childProps,
100
+ children: filteredChildren,
101
+ });
102
+ }
103
+ }
104
+ return child;
105
+ });
106
+ };
107
+ // Helper to extract only CardActions (preserving wrapper components)
108
+ export const extractCardActions = (children, actionType) => {
109
+ const recursiveExtract = (children) => {
110
+ return React.Children.map(children, child => {
111
+ if (!React.isValidElement(child))
112
+ return null;
113
+ // Direct action
114
+ if (child.type === actionType) {
115
+ return child;
116
+ }
117
+ // If this child contains a CardActions wrapper in its tree, keep it
118
+ if (checkForComponentType(child, actionType) && !hasContentInChildren(child, actionType)) {
119
+ return child;
120
+ }
121
+ return null;
122
+ });
123
+ };
124
+ return recursiveExtract(children);
125
+ };
126
+ // Helper that recursively collects onPress or other defined handlers from descendants
127
+ export const collectChildActionHandlers = (children) => React.Children.toArray(children).reduce((handlers, child) => {
128
+ if (React.isValidElement(child)) {
129
+ const childProps = child.props;
130
+ // Check using displayName as CardPressHandler might not be directly importable
131
+ // @ts-expect-error - type
132
+ if (child.type?.displayName === 'CardPressHandler') {
133
+ const actionChildren = React.Children.toArray(childProps.children);
134
+ const handlerToInherit = childProps['handlerToInherit'] || 'onPress';
135
+ const firstChild = actionChildren[0];
136
+ const handler = getInheritableHandler(firstChild, handlerToInherit);
137
+ if (handler) {
138
+ handlers.push(handler);
139
+ }
140
+ }
141
+ if (childProps.children) {
142
+ handlers.push(...collectChildActionHandlers(childProps.children));
143
+ }
144
+ }
145
+ return handlers;
146
+ }, []);
@@ -1,5 +1,7 @@
1
1
  export { default as Card } from './Card';
2
2
  export { useCardContext } from './Card.context';
3
3
  export * from './CardAction';
4
+ export { default as CardActions } from './CardActions';
5
+ export { default as CardContent } from './CardContent';
4
6
  export { default as CardPressHandler } from './CardPressHandler';
5
7
  export { useCardPressHandlerContext } from './CardPressHandler.context';