@campxdev/react-native-blueprint 0.1.16 → 0.1.18

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.
@@ -0,0 +1,376 @@
1
+ // @ts-nocheck
2
+ import {
3
+ View as RNView,
4
+ Text as RNTextBase,
5
+ Image as RNImage,
6
+ type ImageSourcePropType,
7
+ type StyleProp,
8
+ type ViewStyle,
9
+ } from 'react-native';
10
+ import { cssInterop } from 'nativewind';
11
+ import { cva } from 'class-variance-authority';
12
+
13
+ import { cn } from '../../../lib/utils';
14
+ import { Button } from '../../Input/Button/Button';
15
+ import { Badge } from '../Badge/Badge';
16
+ import { Text } from '../../Input/Text/Text';
17
+ import { Avatar } from '../Avatar/Avatar';
18
+ import { AspectRatio } from '../../Layout/AspectRatio/Aspect-Ratio';
19
+
20
+ // Default placeholder image
21
+ const DEFAULT_MEDIA_IMAGE =
22
+ 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe3e?w=400&h=224&fit=crop';
23
+
24
+ // NativeWind interop (className -> style)
25
+ const View = cssInterop(RNView, { className: 'style' });
26
+ const RNText = cssInterop(RNTextBase, { className: 'style' });
27
+ const Image = cssInterop(RNImage, { className: 'style' });
28
+
29
+ /* ============================================================================
30
+ * VARIANTS
31
+ * ============================================================================ */
32
+
33
+ export const FeedCardVariants = {
34
+ type: ['post', 'announcement'] as const,
35
+ } as const;
36
+
37
+ type FeedCardType = (typeof FeedCardVariants.type)[number];
38
+
39
+ const rootVariants = cva('w-full overflow-hidden rounded-3xl', {
40
+ variants: {
41
+ type: {
42
+ post: 'bg-surface-default',
43
+ announcement: 'bg-surface-default border border-border-default',
44
+ },
45
+ },
46
+ defaultVariants: {
47
+ type: 'post',
48
+ },
49
+ });
50
+
51
+ const metaRowVariants = cva('w-full flex-row p-4', {
52
+ variants: {
53
+ type: {
54
+ post: 'items-center gap-2',
55
+ announcement: 'items-center justify-between',
56
+ },
57
+ },
58
+ defaultVariants: {
59
+ type: 'post',
60
+ },
61
+ });
62
+
63
+ const mediaWrapVariants = cva('w-full overflow-hidden px-4 pb-4');
64
+
65
+ const contentWrapVariants = cva('w-full px-4 pb-4 flex-col');
66
+
67
+ const headerVariants = cva('w-full gap-1 flex-col');
68
+
69
+ const titleVariants = cva('text-base font-semibold text-text-primary');
70
+
71
+ const subtitleVariants = cva('text-xs font-medium text-text-secondary');
72
+
73
+ const bodyVariants = cva('text-sm font-medium text-text-primary');
74
+
75
+ const badgesRowVariants = cva('w-full flex-row items-start gap-2 pb-4 px-4');
76
+
77
+ const footerRowVariants = cva(
78
+ 'w-full flex-row items-center justify-between px-4 pb-4'
79
+ );
80
+
81
+ const leftButtonsVariants = cva('flex-row items-center gap-2');
82
+
83
+ const rightButtonsVariants = cva('flex-row items-center');
84
+
85
+ /* ============================================================================
86
+ * HELPERS
87
+ * ============================================================================ */
88
+
89
+ function normalizeType(v: any): FeedCardType {
90
+ const s = String(v ?? 'post').toLowerCase();
91
+ return s === 'announcement' ? 'announcement' : 'post';
92
+ }
93
+
94
+ /* ============================================================================
95
+ * PROPS
96
+ * ============================================================================ */
97
+
98
+ export type FeedCardProps = {
99
+ /** Author/creator name */
100
+ authorName?: string;
101
+
102
+ /** Post/announcement title */
103
+ postTitle?: string;
104
+
105
+ /** Subtitle below title */
106
+ subtitle?: string;
107
+
108
+ /** Main body content */
109
+ postContent?: string;
110
+
111
+ /** Optional image/media */
112
+ image?: ImageSourcePropType;
113
+
114
+ /** Card type variant */
115
+ type?: FeedCardType | 'Post' | 'Announcement';
116
+
117
+ /** Visibility toggles */
118
+ showHeader?: boolean;
119
+ showLeading?: boolean;
120
+ showMedia?: boolean;
121
+ showPostContent?: boolean;
122
+ showSubtitle?: boolean;
123
+ showBadges?: boolean;
124
+ showFooterActions?: boolean;
125
+ showSecondaryButton?: boolean;
126
+
127
+ /** Badge labels */
128
+ badge1Text?: string;
129
+ badge2Text?: string;
130
+
131
+ /** Button labels */
132
+ primaryActionText?: string;
133
+ secondaryActionText?: string;
134
+ tertiaryActionText?: string;
135
+
136
+ /** Callbacks */
137
+ onPrimaryActionPress?: () => void;
138
+ onSecondaryActionPress?: () => void;
139
+ onTertiaryActionPress?: () => void;
140
+
141
+ /** Styling */
142
+ style?: StyleProp<ViewStyle>;
143
+ testID?: string;
144
+ className?: never;
145
+ };
146
+
147
+ /* ============================================================================
148
+ * COMPONENT
149
+ * ============================================================================ */
150
+
151
+ export function FeedCard(props: FeedCardProps) {
152
+ const {
153
+ authorName = 'Author Name',
154
+ postTitle = 'Post Title',
155
+ subtitle = 'Subtitle of the card goes here',
156
+ postContent = 'The card may contain body content which can be truncated to 1-2 lines. Swap Content in Props.',
157
+ image,
158
+
159
+ type = 'post',
160
+
161
+ showHeader = true,
162
+ showLeading = true,
163
+ showMedia = true,
164
+ showPostContent = true,
165
+ showSubtitle = true,
166
+ showBadges = true,
167
+ showFooterActions = true,
168
+ showSecondaryButton = true,
169
+
170
+ badge1Text = 'Badge',
171
+ badge2Text = 'Badge',
172
+
173
+ primaryActionText = 'Button',
174
+ secondaryActionText = 'Button',
175
+
176
+ onPrimaryActionPress,
177
+ onSecondaryActionPress,
178
+ onTertiaryActionPress,
179
+
180
+ style,
181
+ testID,
182
+ } = props;
183
+
184
+ const cardType = normalizeType(type);
185
+ const isAnnouncement = cardType === 'announcement';
186
+
187
+ return (
188
+ <View
189
+ testID={testID ?? 'feed-card'}
190
+ className={cn(rootVariants({ type: cardType }))}
191
+ style={style}
192
+ >
193
+ {/* Meta Section: Author Info */}
194
+ <View
195
+ testID="feed-card-meta"
196
+ className={cn(metaRowVariants({ type: cardType }))}
197
+ >
198
+ {showLeading && (
199
+ <View
200
+ testID="feed-card-leading"
201
+ className="items-center justify-center"
202
+ >
203
+ <Avatar
204
+ initials={(authorName ?? 'AA').slice(0, 2).toUpperCase()}
205
+ size="Default"
206
+ type="Initials"
207
+ />
208
+ </View>
209
+ )}
210
+
211
+ {/* Author Details */}
212
+ <View testID="feed-card-author" className="flex-col flex-1 gap-0.5">
213
+ <RNText
214
+ testID="feed-card-author-name"
215
+ numberOfLines={1}
216
+ className="text-sm font-semibold text-text-primary"
217
+ >
218
+ {authorName}
219
+ </RNText>
220
+ <RNText
221
+ testID="feed-card-timestamp"
222
+ numberOfLines={1}
223
+ className="text-xs font-normal text-text-secondary"
224
+ >
225
+ Posted 1 day ago
226
+ </RNText>
227
+ </View>
228
+
229
+ {/* Announcement Badge */}
230
+ {isAnnouncement && (
231
+ <Badge
232
+ testID="feed-card-type-badge"
233
+ variant="default"
234
+ size="sm"
235
+ showLeftIcon={false}
236
+ showRightIcon={false}
237
+ >
238
+ <Text>Announcement</Text>
239
+ </Badge>
240
+ )}
241
+ </View>
242
+
243
+ {/* Media Section */}
244
+ {showMedia && (
245
+ <View testID="feed-card-media-wrap" className={cn(mediaWrapVariants())}>
246
+ <AspectRatio variant="16:9">
247
+ <View className="w-full h-full bg-surface-subtle rounded-3xl overflow-hidden">
248
+ <Image
249
+ testID="feed-card-media"
250
+ source={image || { uri: DEFAULT_MEDIA_IMAGE }}
251
+ resizeMode="cover"
252
+ className="w-full h-full"
253
+ />
254
+ </View>
255
+ </AspectRatio>
256
+ </View>
257
+ )}
258
+
259
+ {/* Content Section */}
260
+ {showPostContent && (
261
+ <View testID="feed-card-content" className={cn(contentWrapVariants())}>
262
+ {/* Header: Title + Subtitle */}
263
+ {showHeader && (
264
+ <View testID="feed-card-header" className={cn(headerVariants())}>
265
+ <RNText
266
+ testID="feed-card-title"
267
+ numberOfLines={1}
268
+ className={cn(titleVariants())}
269
+ >
270
+ {postTitle}
271
+ </RNText>
272
+
273
+ {showSubtitle && (
274
+ <RNText
275
+ testID="feed-card-subtitle"
276
+ numberOfLines={1}
277
+ className={cn(subtitleVariants())}
278
+ >
279
+ {subtitle}
280
+ </RNText>
281
+ )}
282
+ </View>
283
+ )}
284
+
285
+ {/* Body */}
286
+ <RNText
287
+ testID="feed-card-body"
288
+ numberOfLines={2}
289
+ className={cn(bodyVariants())}
290
+ >
291
+ {postContent}
292
+ </RNText>
293
+ </View>
294
+ )}
295
+
296
+ {/* Badges Section */}
297
+ {showBadges && (
298
+ <View testID="feed-card-badges" className={cn(badgesRowVariants())}>
299
+ <Badge
300
+ testID="feed-card-badge-1"
301
+ variant="default"
302
+ size="sm"
303
+ showLeftIcon={false}
304
+ showRightIcon={false}
305
+ >
306
+ <Text>{badge1Text}</Text>
307
+ </Badge>
308
+ <Badge
309
+ testID="feed-card-badge-2"
310
+ variant="default"
311
+ size="sm"
312
+ showLeftIcon={false}
313
+ showRightIcon={false}
314
+ >
315
+ <Text>{badge2Text}</Text>
316
+ </Badge>
317
+ </View>
318
+ )}
319
+
320
+ {/* Footer Actions */}
321
+ {showFooterActions && (
322
+ <View testID="feed-card-footer" className={cn(footerRowVariants())}>
323
+ <View
324
+ testID="feed-card-footer-left"
325
+ className={cn(leftButtonsVariants())}
326
+ >
327
+ <Button
328
+ testID="feed-card-primary-action"
329
+ disabled={false}
330
+ size="default"
331
+ variant="default"
332
+ showLeftIcon={false}
333
+ showRightIcon={false}
334
+ onPress={onPrimaryActionPress}
335
+ >
336
+ {primaryActionText}
337
+ </Button>
338
+
339
+ {showSecondaryButton && (
340
+ <Button
341
+ testID="feed-card-secondary-action"
342
+ disabled={false}
343
+ size="default"
344
+ variant="secondary"
345
+ showLeftIcon={false}
346
+ showRightIcon={false}
347
+ onPress={onSecondaryActionPress}
348
+ >
349
+ {secondaryActionText}
350
+ </Button>
351
+ )}
352
+ </View>
353
+
354
+ <View
355
+ testID="feed-card-footer-right"
356
+ className={cn(rightButtonsVariants())}
357
+ >
358
+ <Button
359
+ testID="feed-card-tertiary-action"
360
+ disabled={false}
361
+ size="icon"
362
+ variant="secondary"
363
+ showLeftIcon={false}
364
+ showRightIcon={false}
365
+ onPress={onTertiaryActionPress}
366
+ />
367
+ </View>
368
+ </View>
369
+ )}
370
+ </View>
371
+ );
372
+ }
373
+
374
+ FeedCard.displayName = 'FeedCard';
375
+
376
+ export type { FeedCardProps };
@@ -2,11 +2,19 @@ import figma from '@figma/code-connect';
2
2
  import { Greeting } from './Greeting';
3
3
 
4
4
  const FIGMA_URL =
5
- 'https://www.figma.com/design/66WaqopqU3WXgwVtyQuTUf/React-Native-Blueprint-Library?node-id=448-8146';
5
+ 'https://www.figma.com/design/66WaqopqU3WXgwVtyQuTUf/React-Native-Blueprint-Library?node-id=495-8995';
6
6
 
7
7
  figma.connect(Greeting, FIGMA_URL, {
8
8
  props: {
9
+ ctaLayout: figma.enum('CTA Layout', {
10
+ 'icon': 'icon',
11
+ 'none': 'none',
12
+ 'floating': 'floating',
13
+ 'ai summary': 'ai summary',
14
+ }),
9
15
  showNextUp: figma.boolean('Show NextUp'),
10
16
  },
11
- example: (props) => <Greeting showNextUp={props.showNextUp} />,
17
+ example: (props) => (
18
+ <Greeting ctaLayout={props.ctaLayout} showNextUp={props.showNextUp} />
19
+ ),
12
20
  });
@@ -1,18 +1,17 @@
1
1
  // @ts-nocheck
2
2
  import * as React from 'react';
3
- import {
4
- Pressable as RNPressable,
5
- StyleSheet,
6
- View as RNView,
7
- } from 'react-native';
3
+ import { Pressable as RNPressable, View as RNView } from 'react-native';
4
+ import { LinearGradient } from 'expo-linear-gradient';
8
5
  import { cssInterop } from 'nativewind';
9
6
  import { Calendar } from 'lucide-react-native';
10
7
  import { cn } from '../../../lib/utils';
11
8
  import { Text } from '../../Input/Text/Text';
12
9
  import { Icon } from '../../ui/Icon';
10
+ import { Button } from '../../Input/Button/Button';
13
11
 
14
12
  cssInterop(RNView, { className: 'style' });
15
13
  cssInterop(RNPressable, { className: 'style' });
14
+ cssInterop(LinearGradient, { className: 'style' });
16
15
 
17
16
  const View = RNView as React.ComponentType<
18
17
  React.ComponentProps<typeof RNView> & { className?: string }
@@ -31,7 +30,9 @@ export interface GreetingProps {
31
30
  greetingText?: string;
32
31
  /** Subtitle/secondary text below greeting */
33
32
  subtitle?: string;
34
- /** Whether to show the schedule/calendar button */
33
+ /** CTA (Call-to-Action) layout style */
34
+ ctaLayout?: 'icon' | 'none' | 'floating' | 'ai summary';
35
+ /** Whether to show the schedule/calendar button - deprecated, use ctaLayout instead */
35
36
  showScheduleButton?: boolean;
36
37
  /** Callback when schedule button is pressed */
37
38
  onSchedulePress?: () => void;
@@ -77,7 +78,8 @@ export function Greeting({
77
78
  userName = 'Marshall',
78
79
  greetingText = 'Good Morning',
79
80
  subtitle = 'You have 3 Classes today',
80
- showScheduleButton = true,
81
+ ctaLayout = 'icon',
82
+ showScheduleButton,
81
83
  onSchedulePress,
82
84
  showNextUp = true,
83
85
  nextUpTitle = 'Digital Logic Design',
@@ -85,16 +87,51 @@ export function Greeting({
85
87
  className,
86
88
  testID,
87
89
  }: GreetingProps) {
90
+ // Handle backwards compatibility: showScheduleButton prop takes precedence if explicitly set
91
+ const effectiveCtaLayout =
92
+ showScheduleButton !== undefined
93
+ ? showScheduleButton
94
+ ? 'icon'
95
+ : 'none'
96
+ : ctaLayout;
97
+
98
+ const isAiSummary = effectiveCtaLayout === 'ai summary';
99
+ const isFloating = effectiveCtaLayout === 'floating';
100
+ const isIconOrFloatingOrNoneOrAiSummary = [
101
+ 'icon',
102
+ 'floating',
103
+ 'none',
104
+ 'ai summary',
105
+ ].includes(effectiveCtaLayout);
106
+ const isNone = effectiveCtaLayout === 'none';
107
+
88
108
  return (
89
109
  <View
90
110
  testID={testID}
91
111
  className={cn(
92
- 'w-full max-w-[400px] flex-col gap-4 items-start justify-end rounded-lg border border-border-default bg-surface-default px-3 py-4',
112
+ 'w-full max-w-[400px] flex-col items-start justify-end bg-surface-default',
113
+ // Base styling based on ctaLayout
114
+ isAiSummary
115
+ ? 'gap-4 rounded-3xl border border-border-default px-0 pt-4 pb-0 overflow-hidden'
116
+ : isFloating
117
+ ? 'gap-4 rounded-3xl border border-border-default px-0 pt-4 pb-0 overflow-hidden'
118
+ : isNone
119
+ ? 'gap-4 rounded-3xl border border-border-default px-0 py-4'
120
+ : 'gap-4 rounded-3xl border border-border-default px-0 py-4',
93
121
  className
94
122
  )}
95
123
  >
96
124
  {/* Header Section: Greeting + Schedule Button */}
97
- <View className="w-full flex-row items-center justify-between">
125
+ <View
126
+ className={cn(
127
+ 'w-full flex-row items-center',
128
+ isFloating || isAiSummary
129
+ ? 'justify-between px-3'
130
+ : isNone
131
+ ? 'px-3'
132
+ : 'px-3'
133
+ )}
134
+ >
98
135
  {/* Header Content: Title and Subtitle */}
99
136
  <View className="flex-1 flex-col gap-1">
100
137
  <Text className="font-semibold text-base text-text-primary">
@@ -105,22 +142,32 @@ export function Greeting({
105
142
  </Text>
106
143
  </View>
107
144
 
108
- {/* Schedule Button with Calendar Icon */}
109
- {showScheduleButton && (
145
+ {/* Schedule Button with Calendar Icon - Icon Layout */}
146
+ {effectiveCtaLayout === 'icon' && (
147
+ <Pressable
148
+ onPress={onSchedulePress}
149
+ hitSlop={12}
150
+ className="ml-3 flex-row items-center justify-center rounded-lg p-3 bg-highlight-purple"
151
+ >
152
+ <Icon as={Calendar} size={16} color="white" />
153
+ </Pressable>
154
+ )}
155
+
156
+ {/* Calendar Icon for AI Summary and Floating layouts */}
157
+ {isAiSummary && (
110
158
  <Pressable
111
159
  onPress={onSchedulePress}
112
160
  hitSlop={12}
113
- className="ml-3 flex-row items-center justify-center rounded-lg bg-highlight-purple p-3"
114
- style={styles.scheduleButton}
161
+ className="ml-3 flex-row items-center justify-center rounded-lg p-3 bg-highlight-purple"
115
162
  >
116
163
  <Icon as={Calendar} size={16} color="white" />
117
164
  </Pressable>
118
165
  )}
119
166
  </View>
120
167
 
121
- {/* Next Up Section */}
122
- {showNextUp && (
123
- <View className="w-full flex-col gap-1">
168
+ {/* Next Up Section - For icon, none, floating, and ai summary layouts */}
169
+ {isIconOrFloatingOrNoneOrAiSummary && showNextUp && (
170
+ <View className="w-full flex-col gap-1 px-3">
124
171
  <Text className="font-medium text-xs text-text-secondary">
125
172
  Next up
126
173
  </Text>
@@ -137,18 +184,52 @@ export function Greeting({
137
184
  </View>
138
185
  </View>
139
186
  )}
187
+
188
+ {/* AI Summary Button - Purple to Red Gradient - Full Width */}
189
+ {isAiSummary && (
190
+ <LinearGradient
191
+ colors={['#573dab', '#f2353c']}
192
+ start={{ x: 0, y: 0 }}
193
+ end={{ x: 1, y: 0 }}
194
+ className="w-full items-center justify-center"
195
+ style={{
196
+ paddingVertical: 8,
197
+ borderBottomLeftRadius: 20,
198
+ borderBottomRightRadius: 20,
199
+ }}
200
+ >
201
+ <Pressable
202
+ onPress={onSchedulePress}
203
+ className="w-full items-center justify-center"
204
+ style={{ paddingVertical: 0 }}
205
+ >
206
+ <Text className="font-semibold text-sm text-white">
207
+ AI Summary for Today
208
+ </Text>
209
+ </Pressable>
210
+ </LinearGradient>
211
+ )}
212
+
213
+ {/* Floating Button - Full width at bottom - Card color (white) */}
214
+ {isFloating && (
215
+ <View
216
+ className="w-full bg-surface-default"
217
+ style={{ borderBottomLeftRadius: 20, borderBottomRightRadius: 20 }}
218
+ >
219
+ <Button
220
+ variant="outline"
221
+ size="default"
222
+ showRightIcon
223
+ rightIcon={Calendar}
224
+ onPress={onSchedulePress}
225
+ style={{ width: '100%' }}
226
+ >
227
+ View Schedule
228
+ </Button>
229
+ </View>
230
+ )}
140
231
  </View>
141
232
  );
142
233
  }
143
234
 
144
235
  Greeting.displayName = 'Greeting';
145
-
146
- const styles = StyleSheet.create({
147
- scheduleButton: {
148
- shadowColor: '#573dab',
149
- shadowOffset: { width: 0, height: 0 },
150
- shadowOpacity: 0.5,
151
- shadowRadius: 12,
152
- elevation: 8,
153
- },
154
- });
@@ -19,6 +19,7 @@ export * from '../Layout/Bottomsheet/Bottom-Sheet';
19
19
  export * from '../Input/Button/Button';
20
20
  export * from '../DataDisplay/Card/Card';
21
21
  export * from '../DataDisplay/CalendarItem/CalendarItem';
22
+ export * from '../DataDisplay/FeedCard/FeedCard';
22
23
  export * from '../DataDisplay/Greeting/Greeting';
23
24
  export * from '../DataDisplay/MonthCalendar/MonthCalendar';
24
25
  export * from '../DataDisplay/DataCard/DataCard';