@compsych/mobile-ui 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compsych/mobile-ui",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "ComPsych React Native Design System — 19 mobile UI components with self-contained design tokens",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -32,6 +32,7 @@
32
32
  "peerDependencies": {
33
33
  "@expo/vector-icons": ">=15.0.0",
34
34
  "expo-font": ">=13.0.0",
35
+ "lucide-react-native": ">=0.400.0",
35
36
  "react": ">=18.0.0",
36
37
  "react-native": ">=0.73.0",
37
38
  "react-native-svg": ">=15.0.0"
@@ -10,7 +10,7 @@ import {
10
10
 
11
11
  import { Ionicons } from '@expo/vector-icons';
12
12
 
13
- import { ICON_MAP, type IconName } from '../../icons';
13
+ import { type IconName, SIZE_MAP, resolveIcon } from '../../icons';
14
14
  import { useTheme } from '../../theme';
15
15
  import { BodyText } from '../BodyText';
16
16
  import { HeaderText } from '../HeaderText';
@@ -53,7 +53,8 @@ export function SelectionCard({
53
53
  [cr],
54
54
  );
55
55
 
56
- const IconComponent = icon ? ICON_MAP[icon] : null;
56
+ const LucideIcon = icon ? resolveIcon(icon) : null;
57
+ const { size: iconPx, strokeWidth: iconSW } = SIZE_MAP['small'];
57
58
 
58
59
  const a11yState = {
59
60
  disabled,
@@ -102,9 +103,9 @@ export function SelectionCard({
102
103
  style,
103
104
  ]}
104
105
  >
105
- {IconComponent && (
106
+ {LucideIcon && (
106
107
  <View style={styles.smIconWrap}>
107
- <IconComponent size="small" color={iconColor} />
108
+ <LucideIcon size={iconPx} color={iconColor} strokeWidth={iconSW} />
108
109
  </View>
109
110
  )}
110
111
  <BodyText
@@ -131,7 +132,7 @@ export function SelectionCard({
131
132
  ? cr.accent.primary.sysPrimary
132
133
  : cr.surface.surface.sysOnSurface;
133
134
 
134
- const iconCircle = IconComponent ? (
135
+ const iconCircle = LucideIcon ? (
135
136
  <View
136
137
  style={[
137
138
  styles.mdIconCircle,
@@ -142,7 +143,7 @@ export function SelectionCard({
142
143
  },
143
144
  ]}
144
145
  >
145
- <IconComponent size="small" color={iconColor} />
146
+ <LucideIcon size={iconPx} color={iconColor} strokeWidth={iconSW} />
146
147
  </View>
147
148
  ) : null;
148
149
 
@@ -10,9 +10,7 @@ import {
10
10
  ViewStyle,
11
11
  } from 'react-native';
12
12
 
13
- import { Ionicons } from '@expo/vector-icons';
14
-
15
- import { ICON_MAP, type IconName } from '../../icons';
13
+ import { type IconName, SIZE_MAP, resolveIcon } from '../../icons';
16
14
  import { useTheme } from '../../theme';
17
15
  import { BodyText } from '../BodyText';
18
16
 
@@ -146,7 +144,6 @@ export function ServiceCard({
146
144
  iconSize: 24,
147
145
  iconSizeName: 'small' as const,
148
146
  titleVariant: 'medium' as const,
149
- descVariant: 'small' as const,
150
147
  showChevron: true,
151
148
  },
152
149
  md: {
@@ -158,7 +155,6 @@ export function ServiceCard({
158
155
  iconSize: 32,
159
156
  iconSizeName: 'medium' as const,
160
157
  titleVariant: 'medium' as const,
161
- descVariant: 'small' as const,
162
158
  showChevron: false,
163
159
  },
164
160
  lg: {
@@ -170,7 +166,6 @@ export function ServiceCard({
170
166
  iconSize: 32,
171
167
  iconSizeName: 'large' as const,
172
168
  titleVariant: 'large' as const,
173
- descVariant: 'small' as const,
174
169
  showChevron: false,
175
170
  },
176
171
  }),
@@ -181,10 +176,12 @@ export function ServiceCard({
181
176
  const s = sizeTokens[size];
182
177
  const isRow = s.layout === 'row';
183
178
 
184
- const IconComponent = icon ? ICON_MAP[icon] : null;
179
+ const LucideIcon = icon ? resolveIcon(icon) : null;
180
+ const ChevronRight = resolveIcon('ChevronRight');
185
181
  const iconColor = v.iconBadgeColor ?? v.titleColor;
186
- const renderedIcon = IconComponent ? (
187
- <IconComponent size={s.iconSizeName} color={iconColor} />
182
+ const { size: iconPx, strokeWidth: iconSW } = SIZE_MAP[s.iconSizeName];
183
+ const renderedIcon = LucideIcon ? (
184
+ <LucideIcon size={iconPx} color={iconColor} strokeWidth={iconSW} />
188
185
  ) : null;
189
186
 
190
187
  const outerStyle = [
@@ -217,7 +214,7 @@ export function ServiceCard({
217
214
  </BodyText>
218
215
  )}
219
216
  {!isRow && description && (
220
- <BodyText variant={s.descVariant} color={v.descColor}>
217
+ <BodyText variant="small" color={v.descColor}>
221
218
  {description}
222
219
  </BodyText>
223
220
  )}
@@ -289,23 +286,10 @@ export function ServiceCard({
289
286
  {renderedIcon}
290
287
  </View>
291
288
  )}
292
- <View style={styles.contentRow}>
293
- {(title || description) && (
294
- <View style={styles.contentText}>
295
- {title && (
296
- <BodyText variant={s.titleVariant} color={v.titleColor}>
297
- {title}
298
- </BodyText>
299
- )}
300
- {description && (
301
- <BodyText variant={s.descVariant} color={v.descColor}>
302
- {description}
303
- </BodyText>
304
- )}
305
- </View>
306
- )}
307
- {buttonIcon}
308
- </View>
289
+ {textBlock}
290
+ {isRow && ChevronRight && (
291
+ <ChevronRight size={20} color="#ffffff" strokeWidth={1.5} />
292
+ )}
309
293
  {children}
310
294
  </>
311
295
  );
@@ -319,13 +303,8 @@ export function ServiceCard({
319
303
  </View>
320
304
  )}
321
305
  {textBlock}
322
- {s.showChevron && (
323
- <Ionicons
324
- name="chevron-forward"
325
- size={20}
326
- color={v.chevronColor}
327
- accessible={false}
328
- />
306
+ {s.showChevron && ChevronRight && (
307
+ <ChevronRight size={20} color={v.chevronColor} strokeWidth={1.5} />
329
308
  )}
330
309
  {children}
331
310
  </>
@@ -339,24 +318,20 @@ export function ServiceCard({
339
318
  {renderedIcon}
340
319
  </View>
341
320
  )}
342
- <View style={styles.contentRow}>
343
- {(title || description) && (
344
- <View style={styles.contentText}>
345
- {title && (
346
- <BodyText variant={s.titleVariant} color={v.titleColor}>
347
- {title}
348
- </BodyText>
349
- )}
350
- {description && (
351
- <BodyText variant={s.descVariant} color={v.descColor}>
352
- {description}
353
- </BodyText>
354
- )}
355
- </View>
356
- )}
357
- {buttonIcon}
358
- </View>
321
+ {textBlock}
359
322
  {children}
323
+ {buttonIcon && (
324
+ <View
325
+ style={{
326
+ position: 'absolute',
327
+ bottom: s.paddingV,
328
+ right: s.paddingH,
329
+ }}
330
+ pointerEvents="none"
331
+ >
332
+ {buttonIcon}
333
+ </View>
334
+ )}
360
335
  </>
361
336
  );
362
337
  }
@@ -402,16 +377,6 @@ const styles = StyleSheet.create({
402
377
  flex: 1,
403
378
  minWidth: 0,
404
379
  },
405
- contentRow: {
406
- flexDirection: 'row',
407
- alignItems: 'flex-end',
408
- alignSelf: 'stretch',
409
- gap: 16,
410
- },
411
- contentText: {
412
- flex: 1,
413
- minWidth: 0,
414
- },
415
380
  doubledIconBadge: {
416
381
  width: 48,
417
382
  height: 48,
@@ -1,63 +1,16 @@
1
- import { AtomIcon } from './AtomIcon';
2
- import { BinocularsIcon } from './BinocularsIcon';
3
- import { FileChartColumnIncreasingIcon } from './FileChartColumnIncreasingIcon';
4
- import { FlagIcon } from './FlagIcon';
5
- import { GlobeIcon } from './GlobeIcon';
6
- import { GraduationCapIcon } from './GraduationCapIcon';
7
- import { HandHeartIcon } from './HandHeartIcon';
8
- import { HandshakeIcon } from './HandshakeIcon';
9
- import { HazeIcon } from './HazeIcon';
10
- import { HeartHandshakeIcon } from './HeartHandshakeIcon';
11
- import { HourglassIcon } from './HourglassIcon';
12
- import { IdCardIcon } from './IdCardIcon';
13
- import { MessageCirclePlusIcon } from './MessageCirclePlusIcon';
14
- import { MountainSnowIcon } from './MountainSnowIcon';
15
- import { SnowflakeIcon } from './SnowflakeIcon';
16
- import { StethoscopeIcon } from './StethoscopeIcon';
17
- import { UserRoundIcon } from './UserRoundIcon';
18
- import { WheatIcon } from './WheatIcon';
19
- import type { IconName, IconProps } from './types';
1
+ import type { LucideIcon } from 'lucide-react-native';
20
2
 
21
3
  export type { IconProps, IconSize, IconName } from './types';
4
+ export { DEFAULT_ICON_COLOR, SIZE_MAP } from './types';
22
5
 
23
- export { UserRoundIcon } from './UserRoundIcon';
24
- export { GlobeIcon } from './GlobeIcon';
25
- export { HandshakeIcon } from './HandshakeIcon';
26
- export { HeartHandshakeIcon } from './HeartHandshakeIcon';
27
- export { AtomIcon } from './AtomIcon';
28
- export { HazeIcon } from './HazeIcon';
29
- export { HourglassIcon } from './HourglassIcon';
30
- export { GraduationCapIcon } from './GraduationCapIcon';
31
- export { HandHeartIcon } from './HandHeartIcon';
32
- export { IdCardIcon } from './IdCardIcon';
33
- export { MessageCirclePlusIcon } from './MessageCirclePlusIcon';
34
- export { StethoscopeIcon } from './StethoscopeIcon';
35
- export { BinocularsIcon } from './BinocularsIcon';
36
- export { FlagIcon } from './FlagIcon';
37
- export { MountainSnowIcon } from './MountainSnowIcon';
38
- export { SnowflakeIcon } from './SnowflakeIcon';
39
- export { FileChartColumnIncreasingIcon } from './FileChartColumnIncreasingIcon';
40
- export { WheatIcon } from './WheatIcon';
6
+ /**
7
+ * Resolves an icon name to a Lucide React Native component.
8
+ * Accepts both "StethoscopeIcon" (trailing Icon suffix) and "Stethoscope".
9
+ * Returns undefined if the icon name is not found in lucide-react-native.
10
+ */
11
+ export function resolveIcon(name: string): LucideIcon | undefined {
12
+ const componentName = name.endsWith('Icon') ? name.slice(0, -4) : name;
41
13
 
42
- type IconComponent = (props: IconProps) => React.JSX.Element;
43
-
44
- export const ICON_MAP: Record<IconName, IconComponent> = {
45
- UserRoundIcon,
46
- GlobeIcon,
47
- HandshakeIcon,
48
- HeartHandshakeIcon,
49
- AtomIcon,
50
- HazeIcon,
51
- HourglassIcon,
52
- GraduationCapIcon,
53
- HandHeartIcon,
54
- IdCardIcon,
55
- MessageCirclePlusIcon,
56
- StethoscopeIcon,
57
- BinocularsIcon,
58
- FlagIcon,
59
- MountainSnowIcon,
60
- SnowflakeIcon,
61
- FileChartColumnIncreasingIcon,
62
- WheatIcon,
63
- };
14
+ const icons = require('lucide-react-native') as Record<string, unknown>;
15
+ return icons[componentName] as LucideIcon | undefined;
16
+ }
@@ -2,25 +2,9 @@ import { sys } from '../tokens';
2
2
 
3
3
  export type IconSize = 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge';
4
4
 
5
- export type IconName =
6
- | 'UserRoundIcon'
7
- | 'GlobeIcon'
8
- | 'HandshakeIcon'
9
- | 'HeartHandshakeIcon'
10
- | 'AtomIcon'
11
- | 'HazeIcon'
12
- | 'HourglassIcon'
13
- | 'GraduationCapIcon'
14
- | 'HandHeartIcon'
15
- | 'IdCardIcon'
16
- | 'MessageCirclePlusIcon'
17
- | 'StethoscopeIcon'
18
- | 'BinocularsIcon'
19
- | 'FlagIcon'
20
- | 'MountainSnowIcon'
21
- | 'SnowflakeIcon'
22
- | 'FileChartColumnIncreasingIcon'
23
- | 'WheatIcon';
5
+ /** Any Lucide icon component name, with or without the trailing "Icon" suffix.
6
+ * Examples: "Stethoscope", "StethoscopeIcon", "UserRound", "UserRoundIcon" */
7
+ export type IconName = string;
24
8
 
25
9
  export const DEFAULT_ICON_COLOR = sys.colorRoles.surface.surface.sysOnSurface;
26
10
 
@@ -32,10 +16,10 @@ export interface IconProps {
32
16
  export const SIZE_MAP: Record<IconSize, { size: number; strokeWidth: number }> =
33
17
  {
34
18
  xsmall: { size: 16, strokeWidth: 1 },
35
- small: { size: 20, strokeWidth: 1.5 },
36
- medium: { size: 24, strokeWidth: 2 },
37
- large: { size: 32, strokeWidth: 2 },
38
- xlarge: { size: 48, strokeWidth: 2 },
19
+ small: { size: 20, strokeWidth: 1 },
20
+ medium: { size: 24, strokeWidth: 1.5 },
21
+ large: { size: 32, strokeWidth: 1.5 },
22
+ xlarge: { size: 48, strokeWidth: 1.5 },
39
23
  };
40
24
 
41
25
  export const COMMON = {
package/src/index.ts CHANGED
@@ -35,14 +35,6 @@ export type {
35
35
  ServiceCardSize,
36
36
  } from './components/ServiceCard';
37
37
 
38
- export { PromotionCard } from './components/PromotionCard';
39
- export type {
40
- PromotionCardProps,
41
- PromotionCardVariant,
42
- PromotionCardUsage,
43
- PromotionCardSize,
44
- } from './components/PromotionCard';
45
-
46
38
  export { Checkbox } from './components/Checkbox';
47
39
  export type {
48
40
  CheckboxProps,
@@ -153,26 +145,6 @@ export { sys } from './tokens';
153
145
  export { ThemeProvider, useTheme, defaultTheme } from './theme';
154
146
  export type { ThemeProviderProps, Theme, ThemeOverride } from './theme';
155
147
 
156
- // Icons
157
- export {
158
- UserRoundIcon,
159
- GlobeIcon,
160
- HandshakeIcon,
161
- HeartHandshakeIcon,
162
- AtomIcon,
163
- HazeIcon,
164
- HourglassIcon,
165
- GraduationCapIcon,
166
- HandHeartIcon,
167
- IdCardIcon,
168
- MessageCirclePlusIcon,
169
- StethoscopeIcon,
170
- BinocularsIcon,
171
- FlagIcon,
172
- MountainSnowIcon,
173
- SnowflakeIcon,
174
- FileChartColumnIncreasingIcon,
175
- WheatIcon,
176
- } from './icons';
148
+ // Icons — resolved from lucide-react-native at runtime
149
+ export { resolveIcon, SIZE_MAP, DEFAULT_ICON_COLOR } from './icons';
177
150
  export type { IconProps, IconSize, IconName } from './icons';
178
- export { ICON_MAP } from './icons';
@@ -1,73 +0,0 @@
1
- import React from 'react';
2
-
3
- import { fireEvent, render } from '@testing-library/react-native';
4
-
5
- import { PromotionCard } from './index';
6
-
7
- describe('PromotionCard', () => {
8
- it('renders title and description', () => {
9
- const { getByText } = render(
10
- <PromotionCard title="New feature" description="Check it out" />,
11
- );
12
- expect(getByText('New feature')).toBeTruthy();
13
- expect(getByText('Check it out')).toBeTruthy();
14
- });
15
-
16
- it('renders chip label when provided', () => {
17
- const { getByText } = render(
18
- <PromotionCard title="Card" chipLabel="NEW" />,
19
- );
20
- expect(getByText('NEW')).toBeTruthy();
21
- });
22
-
23
- it('calls onPress when tapped', () => {
24
- const onPress = jest.fn();
25
- const { getByRole } = render(
26
- <PromotionCard
27
- title="Promo"
28
- accessibilityLabel="Promo card"
29
- onPress={onPress}
30
- />,
31
- );
32
- fireEvent.press(getByRole('button', { name: 'Promo card' }));
33
- expect(onPress).toHaveBeenCalledTimes(1);
34
- });
35
-
36
- it('renders disabled state without crashing', () => {
37
- const { toJSON } = render(
38
- <PromotionCard
39
- title="Promo"
40
- accessibilityLabel="Promo card"
41
- onPress={jest.fn()}
42
- disabled
43
- />,
44
- );
45
- expect(toJSON()).toBeTruthy();
46
- });
47
-
48
- it.each(['filled', 'tonal'] as const)(
49
- 'renders variant "%s" without crashing',
50
- (variant) => {
51
- const { toJSON } = render(
52
- <PromotionCard title="Card" variant={variant} />,
53
- );
54
- expect(toJSON()).toBeTruthy();
55
- },
56
- );
57
-
58
- it.each(['md', 'lg'] as const)(
59
- 'renders size "%s" without crashing',
60
- (size) => {
61
- const { toJSON } = render(<PromotionCard title="Card" size={size} />);
62
- expect(toJSON()).toBeTruthy();
63
- },
64
- );
65
-
66
- it.each(['neutral', 'informative', 'positive', 'danger', 'warning'] as const)(
67
- 'renders usage "%s" without crashing',
68
- (usage) => {
69
- const { toJSON } = render(<PromotionCard title="Card" usage={usage} />);
70
- expect(toJSON()).toBeTruthy();
71
- },
72
- );
73
- });