@utilitywarehouse/hearth-react-native 0.16.2 → 0.17.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 (63) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +14 -14
  3. package/CHANGELOG.md +132 -0
  4. package/build/components/Card/CardAction/CardActionRoot.js +12 -2
  5. package/build/components/Card/CardActions.context.d.ts +6 -0
  6. package/build/components/Card/CardActions.context.js +5 -0
  7. package/build/components/Card/CardActions.d.ts +7 -0
  8. package/build/components/Card/CardActions.js +29 -0
  9. package/build/components/Card/CardRoot.js +16 -104
  10. package/build/components/Card/helpers.d.ts +8 -0
  11. package/build/components/Card/helpers.js +146 -0
  12. package/build/components/Card/index.d.ts +2 -0
  13. package/build/components/Card/index.js +2 -0
  14. package/build/components/ExpandableCard/ExpandableCardGroup.d.ts +1 -1
  15. package/build/components/ExpandableCard/ExpandableCardGroup.js +2 -2
  16. package/build/components/ExpandableCard/ExpandableCardGroup.props.d.ts +4 -0
  17. package/build/components/Input/Input.js +4 -3
  18. package/build/components/Input/Input.props.d.ts +9 -0
  19. package/build/components/List/List.context.d.ts +4 -2
  20. package/build/components/List/List.context.js +0 -2
  21. package/build/components/List/List.d.ts +1 -1
  22. package/build/components/List/List.js +25 -38
  23. package/build/components/List/List.props.d.ts +1 -0
  24. package/build/components/List/ListAction/ListAction.js +24 -7
  25. package/build/components/List/ListAction/ListAction.props.d.ts +1 -0
  26. package/build/components/List/ListItem/ListItemRoot.js +12 -4
  27. package/build/utils/isThemedImageProps.d.ts +1 -1
  28. package/package.json +3 -3
  29. package/src/components/Card/Card.docs.mdx +224 -66
  30. package/src/components/Card/Card.stories.tsx +29 -25
  31. package/src/components/Card/CardAction/CardAction.stories.tsx +239 -93
  32. package/src/components/Card/CardAction/CardActionRoot.tsx +15 -2
  33. package/src/components/Card/CardActions.context.ts +12 -0
  34. package/src/components/Card/CardActions.tsx +40 -0
  35. package/src/components/Card/CardRoot.tsx +27 -132
  36. package/src/components/Card/helpers.tsx +195 -0
  37. package/src/components/Card/index.ts +2 -0
  38. package/src/components/ExpandableCard/ExpandableCard.figma.tsx +33 -38
  39. package/src/components/ExpandableCard/ExpandableCardGroup.figma.tsx +34 -17
  40. package/src/components/ExpandableCard/ExpandableCardGroup.props.ts +5 -0
  41. package/src/components/ExpandableCard/ExpandableCardGroup.tsx +2 -0
  42. package/src/components/HighlightBanner/HighlightBanner.figma.tsx +46 -0
  43. package/src/components/IconButton/IconButton.figma.tsx +20 -30
  44. package/src/components/IconContainer/IconContainer.figma.tsx +7 -13
  45. package/src/components/IndicatorIconButton/IndicatorIconButton.figma.tsx +16 -0
  46. package/src/components/Input/Input.docs.mdx +55 -15
  47. package/src/components/Input/Input.figma.tsx +106 -40
  48. package/src/components/Input/Input.props.ts +9 -0
  49. package/src/components/Input/Input.tsx +21 -0
  50. package/src/components/Link/Link.figma.tsx +31 -38
  51. package/src/components/List/List.context.ts +2 -4
  52. package/src/components/List/List.docs.mdx +10 -5
  53. package/src/components/List/List.figma.tsx +42 -28
  54. package/src/components/List/List.props.ts +1 -0
  55. package/src/components/List/List.stories.tsx +43 -0
  56. package/src/components/List/List.tsx +38 -51
  57. package/src/components/List/ListAction/ListAction.figma.tsx +5 -13
  58. package/src/components/List/ListAction/ListAction.props.ts +1 -0
  59. package/src/components/List/ListAction/ListAction.tsx +40 -10
  60. package/src/components/List/ListItem/ListItem.figma.tsx +43 -27
  61. package/src/components/List/ListItem/ListItemRoot.tsx +15 -4
  62. package/src/utils/isThemedImageProps.ts +1 -1
  63. package/src/components/InlineLink/InlineLink.figma.tsx +0 -33
@@ -2,9 +2,12 @@ import { Meta, StoryObj } from '@storybook/react-native';
2
2
  import * as Icons from '@utilitywarehouse/hearth-react-native-icons';
3
3
  import { ElectricityMediumIcon, GasMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
4
4
  import { View } from 'react-native';
5
+ import { BodyText } from '../../';
5
6
  import { Badge } from '../../Badge';
6
7
  import { Flex } from '../../Flex';
7
8
  import Card from '../Card';
9
+ import CardActions from '../CardActions';
10
+ import CardContent from '../CardContent';
8
11
  import CardAction from './CardAction';
9
12
 
10
13
  const meta: Meta<typeof CardAction> = {
@@ -74,9 +77,11 @@ export const Playground: Story = {
74
77
  // @ts-expect-error - This is a playground
75
78
  const trailingIcon = args.trailingIcon === 'none' ? undefined : Icons[args.trailingIcon];
76
79
  return (
77
- <View style={{ width: '100%', maxWidth: 400 }}>
80
+ <View style={{ width: '100%', maxWidth: 400, gap: 16 }}>
78
81
  <Card variant="emphasis">
79
- <CardAction {...args} leadingIcon={leadingIcon} trailingIcon={trailingIcon} />
82
+ <CardActions>
83
+ <CardAction {...args} leadingIcon={leadingIcon} trailingIcon={trailingIcon} />
84
+ </CardActions>
80
85
  </Card>
81
86
  </View>
82
87
  );
@@ -90,12 +95,14 @@ export const WithLeadingIcon: Story = {
90
95
  render: () => (
91
96
  <View style={{ width: '100%', maxWidth: 400 }}>
92
97
  <Card variant="emphasis">
93
- <CardAction
94
- heading="Bills"
95
- helperText="View your bills"
96
- leadingIcon={ElectricityMediumIcon}
97
- onPress={() => console.log('pressed')}
98
- />
98
+ <CardActions>
99
+ <CardAction
100
+ heading="Bills"
101
+ helperText="View your bills"
102
+ leadingIcon={ElectricityMediumIcon}
103
+ onPress={() => console.log('pressed')}
104
+ />
105
+ </CardActions>
99
106
  </Card>
100
107
  </View>
101
108
  ),
@@ -108,12 +115,14 @@ export const WithTrailingIcon: Story = {
108
115
  render: () => (
109
116
  <View style={{ width: '100%', maxWidth: 400 }}>
110
117
  <Card variant="emphasis">
111
- <CardAction
112
- heading="Bills"
113
- helperText="View your bills"
114
- trailingIcon={ElectricityMediumIcon}
115
- onPress={() => console.log('pressed')}
116
- />
118
+ <CardActions>
119
+ <CardAction
120
+ heading="Bills"
121
+ helperText="View your bills"
122
+ trailingIcon={ElectricityMediumIcon}
123
+ onPress={() => console.log('pressed')}
124
+ />
125
+ </CardActions>
117
126
  </Card>
118
127
  </View>
119
128
  ),
@@ -127,33 +136,35 @@ export const WithIconContainer: Story = {
127
136
  render: () => (
128
137
  <View style={{ width: '100%', maxWidth: 400 }}>
129
138
  <Card variant="emphasis">
130
- <CardAction
131
- heading="Electricity"
132
- helperText="Last reading 23/03/24"
133
- leadingIcon={ElectricityMediumIcon}
134
- iconContainer
135
- iconContainerVariant="emphasis"
136
- iconContainerColor="energy"
137
- onPress={() => console.log('pressed')}
138
- />
139
- <CardAction
140
- heading="Gas"
141
- helperText="Last reading 23/03/24"
142
- leadingIcon={GasMediumIcon}
143
- iconContainer
144
- iconContainerVariant="emphasis"
145
- iconContainerColor="energy"
146
- onPress={() => console.log('pressed')}
147
- />
148
- <CardAction
149
- heading="Gas"
150
- helperText="Last reading 23/03/24"
151
- leadingIcon={GasMediumIcon}
152
- iconContainer
153
- iconContainerVariant="emphasis"
154
- iconContainerColor="energy"
155
- onPress={() => console.log('pressed')}
156
- />
139
+ <CardActions>
140
+ <CardAction
141
+ heading="Electricity"
142
+ helperText="Last reading 23/03/24"
143
+ leadingIcon={ElectricityMediumIcon}
144
+ iconContainer
145
+ iconContainerVariant="emphasis"
146
+ iconContainerColor="energy"
147
+ onPress={() => console.log('pressed')}
148
+ />
149
+ <CardAction
150
+ heading="Gas"
151
+ helperText="Last reading 23/03/24"
152
+ leadingIcon={GasMediumIcon}
153
+ iconContainer
154
+ iconContainerVariant="emphasis"
155
+ iconContainerColor="energy"
156
+ onPress={() => console.log('pressed')}
157
+ />
158
+ <CardAction
159
+ heading="Gas"
160
+ helperText="Last reading 23/03/24"
161
+ leadingIcon={GasMediumIcon}
162
+ iconContainer
163
+ iconContainerVariant="emphasis"
164
+ iconContainerColor="energy"
165
+ onPress={() => console.log('pressed')}
166
+ />
167
+ </CardActions>
157
168
  </Card>
158
169
  </View>
159
170
  ),
@@ -166,34 +177,40 @@ export const WithBadge: Story = {
166
177
  render: () => (
167
178
  <Flex direction="column" space="md" style={{ width: '100%', maxWidth: 400 }}>
168
179
  <Card variant="emphasis">
169
- <CardAction
170
- heading="Badge at bottom"
171
- helperText="Badge positioned below text"
172
- leadingIcon={ElectricityMediumIcon}
173
- badge={<Badge text="New" />}
174
- badgePosition="bottom"
175
- onPress={() => console.log('pressed')}
176
- />
180
+ <CardActions>
181
+ <CardAction
182
+ heading="Badge at bottom"
183
+ helperText="Badge positioned below text"
184
+ leadingIcon={ElectricityMediumIcon}
185
+ badge={<Badge text="New" />}
186
+ badgePosition="bottom"
187
+ onPress={() => console.log('pressed')}
188
+ />
189
+ </CardActions>
177
190
  </Card>
178
191
  <Card variant="emphasis">
179
- <CardAction
180
- heading="Badge at middle"
181
- helperText="Badge positioned between heading and helper text"
182
- leadingIcon={ElectricityMediumIcon}
183
- badge={<Badge text="New" />}
184
- badgePosition="middle"
185
- onPress={() => console.log('pressed')}
186
- />
192
+ <CardActions>
193
+ <CardAction
194
+ heading="Badge at middle"
195
+ helperText="Badge positioned between heading and helper text"
196
+ leadingIcon={ElectricityMediumIcon}
197
+ badge={<Badge text="New" />}
198
+ badgePosition="middle"
199
+ onPress={() => console.log('pressed')}
200
+ />
201
+ </CardActions>
187
202
  </Card>
188
203
  <Card variant="emphasis">
189
- <CardAction
190
- heading="Badge at right"
191
- helperText="Badge positioned on the right side"
192
- leadingIcon={ElectricityMediumIcon}
193
- badge={<Badge text="New" />}
194
- badgePosition="right"
195
- onPress={() => console.log('pressed')}
196
- />
204
+ <CardActions>
205
+ <CardAction
206
+ heading="Badge at right"
207
+ helperText="Badge positioned on the right side"
208
+ leadingIcon={ElectricityMediumIcon}
209
+ badge={<Badge text="New" />}
210
+ badgePosition="right"
211
+ onPress={() => console.log('pressed')}
212
+ />
213
+ </CardActions>
197
214
  </Card>
198
215
  </Flex>
199
216
  ),
@@ -206,22 +223,26 @@ export const Sizes: Story = {
206
223
  render: () => (
207
224
  <Flex direction="column" space="md" style={{ width: '100%', maxWidth: 400 }}>
208
225
  <Card variant="emphasis">
209
- <CardAction
210
- heading="Medium size (default)"
211
- helperText="Heading size is md"
212
- size="md"
213
- leadingIcon={ElectricityMediumIcon}
214
- onPress={() => console.log('pressed')}
215
- />
226
+ <CardActions>
227
+ <CardAction
228
+ heading="Medium size (default)"
229
+ helperText="Heading size is md"
230
+ size="md"
231
+ leadingIcon={ElectricityMediumIcon}
232
+ onPress={() => console.log('pressed')}
233
+ />
234
+ </CardActions>
216
235
  </Card>
217
236
  <Card variant="emphasis">
218
- <CardAction
219
- heading="Large size"
220
- helperText="Heading size is lg"
221
- size="lg"
222
- leadingIcon={ElectricityMediumIcon}
223
- onPress={() => console.log('pressed')}
224
- />
237
+ <CardActions>
238
+ <CardAction
239
+ heading="Large size"
240
+ helperText="Heading size is lg"
241
+ size="lg"
242
+ leadingIcon={ElectricityMediumIcon}
243
+ onPress={() => console.log('pressed')}
244
+ />
245
+ </CardActions>
225
246
  </Card>
226
247
  </Flex>
227
248
  ),
@@ -234,13 +255,15 @@ export const Loading: Story = {
234
255
  render: () => (
235
256
  <View style={{ width: '100%', maxWidth: 400 }}>
236
257
  <Card variant="emphasis">
237
- <CardAction
238
- heading="Loading"
239
- helperText="This is loading"
240
- loading
241
- leadingIcon={ElectricityMediumIcon}
242
- onPress={() => console.log('pressed')}
243
- />
258
+ <CardActions>
259
+ <CardAction
260
+ heading="Loading"
261
+ helperText="This is loading"
262
+ loading
263
+ leadingIcon={ElectricityMediumIcon}
264
+ onPress={() => console.log('pressed')}
265
+ />
266
+ </CardActions>
244
267
  </Card>
245
268
  </View>
246
269
  ),
@@ -253,14 +276,137 @@ export const Disabled: Story = {
253
276
  render: () => (
254
277
  <View style={{ width: '100%', maxWidth: 400 }}>
255
278
  <Card variant="emphasis">
256
- <CardAction
257
- heading="Disabled"
258
- helperText="This is disabled"
259
- disabled
260
- leadingIcon={ElectricityMediumIcon}
261
- onPress={() => console.log('pressed')}
262
- />
279
+ <CardActions>
280
+ <CardAction
281
+ heading="Disabled"
282
+ helperText="This is disabled"
283
+ disabled
284
+ leadingIcon={ElectricityMediumIcon}
285
+ onPress={() => console.log('pressed')}
286
+ />
287
+ </CardActions>
263
288
  </Card>
264
289
  </View>
265
290
  ),
266
291
  };
292
+
293
+ const CustomAction = ({ heading, ...props }: { heading: string }) => {
294
+ return <CardAction key={heading} heading={heading} {...props} onPress={() => null} />;
295
+ };
296
+
297
+ const CustomComponent = () => <BodyText>Multiple CardActions within a Card:</BodyText>;
298
+
299
+ export const WithCustomActions: Story = {
300
+ parameters: {
301
+ controls: { include: [] },
302
+ },
303
+ render: (args: any) => {
304
+ // @ts-expect-error - This is a playground
305
+ const leadingIcon = args.leadingIcon === 'none' ? undefined : Icons[args.leadingIcon];
306
+ // @ts-expect-error - This is a playground
307
+ const trailingIcon = args.trailingIcon === 'none' ? undefined : Icons[args.trailingIcon];
308
+
309
+ const actions = [{ text: 'Action 1' }, { text: 'Action 2' }, { text: 'Action 3' }];
310
+ return (
311
+ <View style={{ width: '100%', maxWidth: 400, gap: 16 }}>
312
+ {/* Example 1 */}
313
+ <Card variant="emphasis">
314
+ <BodyText>Multiple CardActions within a Card:</BodyText>
315
+ <CardActions>
316
+ {actions.map(action => (
317
+ <CustomAction
318
+ key={action.text}
319
+ {...args}
320
+ heading={action.text}
321
+ leadingIcon={leadingIcon}
322
+ trailingIcon={trailingIcon}
323
+ />
324
+ ))}
325
+ <CardAction
326
+ {...args}
327
+ leadingIcon={leadingIcon}
328
+ trailingIcon={trailingIcon}
329
+ onPress={() => null}
330
+ />
331
+ </CardActions>
332
+ </Card>
333
+ {/* Example 2 */}
334
+ <Card variant="emphasis">
335
+ <CardActions>
336
+ {actions.map(action => (
337
+ <CustomAction
338
+ key={action.text}
339
+ {...args}
340
+ heading={action.text}
341
+ leadingIcon={leadingIcon}
342
+ trailingIcon={trailingIcon}
343
+ />
344
+ ))}
345
+ </CardActions>
346
+ </Card>
347
+ {/* Example 3 */}
348
+ <Card variant="emphasis">
349
+ <CardActions>
350
+ <CardAction
351
+ {...args}
352
+ leadingIcon={leadingIcon}
353
+ trailingIcon={trailingIcon}
354
+ onPress={() => null}
355
+ />
356
+ <CardAction
357
+ {...args}
358
+ leadingIcon={leadingIcon}
359
+ trailingIcon={trailingIcon}
360
+ onPress={() => null}
361
+ />
362
+ </CardActions>
363
+ </Card>
364
+ {/* Example 4 */}
365
+ <Card variant="emphasis">
366
+ <BodyText>Multiple CardActions within a Card:</BodyText>
367
+ </Card>
368
+ {/* Example 5 */}
369
+ <Card variant="emphasis">
370
+ <BodyText>Multiple CardActions within a Card:</BodyText>
371
+ <CardActions>
372
+ <CardAction
373
+ {...args}
374
+ leadingIcon={leadingIcon}
375
+ trailingIcon={trailingIcon}
376
+ onPress={() => null}
377
+ />
378
+ <CardAction
379
+ {...args}
380
+ leadingIcon={leadingIcon}
381
+ trailingIcon={trailingIcon}
382
+ onPress={() => null}
383
+ />
384
+ </CardActions>
385
+ </Card>
386
+ {/* Example 6 */}
387
+ <Card variant="emphasis">
388
+ <CardContent>
389
+ <CustomComponent />
390
+ </CardContent>
391
+ <CardActions>
392
+ {actions.map(action => (
393
+ <CustomAction
394
+ key={action.text}
395
+ {...args}
396
+ heading={action.text}
397
+ leadingIcon={leadingIcon}
398
+ trailingIcon={trailingIcon}
399
+ />
400
+ ))}
401
+ <CardAction
402
+ {...args}
403
+ leadingIcon={leadingIcon}
404
+ trailingIcon={trailingIcon}
405
+ onPress={() => null}
406
+ />
407
+ </CardActions>
408
+ </Card>
409
+ </View>
410
+ );
411
+ },
412
+ };
@@ -1,10 +1,11 @@
1
1
  import { ChevronRightSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
2
- import { useMemo } from 'react';
2
+ import { useId, useLayoutEffect, useMemo } from 'react';
3
3
  import { Pressable, View, ViewStyle } from 'react-native';
4
4
  import { StyleSheet } from 'react-native-unistyles';
5
5
  import { IconContainer } from '../../IconContainer';
6
6
  import { Skeleton } from '../../Skeleton';
7
7
  import { useCardContext } from '../Card.context';
8
+ import { useCardActionsContext } from '../CardActions.context';
8
9
  import { CardActionContext, ICardActionContext } from './CardAction.context';
9
10
  import type CardActionProps from './CardAction.props';
10
11
  import CardActionContent from './CardActionContent';
@@ -45,7 +46,19 @@ const CardActionRoot = ({
45
46
  const loadingTestID = isLoading ? `${testID}-loading` : testID;
46
47
 
47
48
  const { variant, hasOnlyActions } = useCardContext();
48
- const isFirst = props.isFirst;
49
+ const actionId = useId();
50
+ const actionsContext = useCardActionsContext();
51
+
52
+ useLayoutEffect(() => {
53
+ if (!actionsContext) {
54
+ return;
55
+ }
56
+
57
+ return actionsContext.registerAction(actionId);
58
+ }, [actionId, actionsContext]);
59
+
60
+ const isFirstFromContext = actionsContext?.firstActionId === actionId;
61
+ const isFirst = props.isFirst ?? isFirstFromContext;
49
62
 
50
63
  styles.useVariants({
51
64
  showPressed,
@@ -0,0 +1,12 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ export interface CardActionsContextValue {
4
+ firstActionId?: string;
5
+ registerAction: (id: string) => () => void;
6
+ }
7
+
8
+ export const CardActionsContext = createContext<CardActionsContextValue | null>(null);
9
+
10
+ export const useCardActionsContext = (): CardActionsContextValue | null => {
11
+ return useContext(CardActionsContext);
12
+ };
@@ -0,0 +1,40 @@
1
+ import { PropsWithChildren, useCallback, useRef, useState } from 'react';
2
+ import { View, ViewProps } from 'react-native';
3
+ import { StyleSheet } from 'react-native-unistyles';
4
+ import { CardActionsContext } from './CardActions.context';
5
+
6
+ const CardActions = ({ children, style, ...props }: PropsWithChildren<ViewProps>) => {
7
+ const orderRef = useRef<string[]>([]);
8
+ const [firstActionId, setFirstActionId] = useState<string | undefined>(undefined);
9
+
10
+ const registerAction = useCallback((id: string) => {
11
+ if (!orderRef.current.includes(id)) {
12
+ orderRef.current.push(id);
13
+ }
14
+ const nextFirst = orderRef.current[0];
15
+ setFirstActionId(prev => (prev === nextFirst ? prev : nextFirst));
16
+ return () => {
17
+ orderRef.current = orderRef.current.filter(currentId => currentId !== id);
18
+ const nextFirst = orderRef.current[0];
19
+ setFirstActionId(prev => (prev === nextFirst ? prev : nextFirst));
20
+ };
21
+ }, []);
22
+
23
+ return (
24
+ <CardActionsContext.Provider value={{ firstActionId, registerAction }}>
25
+ <View {...props} style={[styles.container, style]}>
26
+ {children}
27
+ </View>
28
+ </CardActionsContext.Provider>
29
+ );
30
+ };
31
+
32
+ CardActions.displayName = 'CardActions';
33
+
34
+ const styles = StyleSheet.create({
35
+ container: {
36
+ width: '100%',
37
+ },
38
+ });
39
+
40
+ export default CardActions;