@compsych/mobile-ui 1.0.5 → 1.0.8

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.5",
3
+ "version": "1.0.8",
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",
@@ -0,0 +1,209 @@
1
+ import React from 'react';
2
+ import {
3
+ Modal,
4
+ Pressable,
5
+ ScrollView,
6
+ StyleSheet,
7
+ View,
8
+ } from 'react-native';
9
+ import { Ionicons } from '@expo/vector-icons';
10
+ import { sys } from './tokens';
11
+ import { HeaderText } from './HeaderText';
12
+ import { Button } from './Button';
13
+
14
+ export interface ActionSheetAction {
15
+ label: string;
16
+ onPress: () => void;
17
+ }
18
+
19
+ export interface ActionSheetProps {
20
+ visible: boolean;
21
+ onClose: () => void;
22
+ title?: string;
23
+ children?: React.ReactNode;
24
+ primaryAction?: ActionSheetAction;
25
+ secondaryAction?: ActionSheetAction;
26
+ }
27
+
28
+ const { colorRoles: cr, dimensions: dim } = sys;
29
+
30
+ export function ActionSheet({
31
+ visible,
32
+ onClose,
33
+ title,
34
+ children,
35
+ primaryAction,
36
+ secondaryAction,
37
+ }: ActionSheetProps) {
38
+ return (
39
+ <Modal
40
+ visible={visible}
41
+ transparent
42
+ animationType="slide"
43
+ onRequestClose={onClose}
44
+ statusBarTranslucent
45
+ >
46
+ {/* Backdrop */}
47
+ <Pressable style={styles.backdrop} onPress={onClose} accessible={false} />
48
+
49
+ {/* Sheet */}
50
+ <View style={styles.sheetWrapper} pointerEvents="box-none">
51
+ <View style={styles.sheet}>
52
+
53
+ {/* Toolbar */}
54
+ <View style={styles.toolbar}>
55
+ {/* Grabber */}
56
+ <View style={styles.grabberRow}>
57
+ <View style={styles.grabber} />
58
+ </View>
59
+
60
+ {/* Title row */}
61
+ <View style={styles.titleRow}>
62
+ {/* Invisible spacer to balance the close button */}
63
+ <View style={styles.closeButton} />
64
+
65
+ <HeaderText
66
+ variant="titleSmall"
67
+ emphasized
68
+ color={cr.surface.surface.sysOnSurface}
69
+ style={styles.titleText}
70
+ >
71
+ {title ?? ''}
72
+ </HeaderText>
73
+
74
+ {/* Close button */}
75
+ <Pressable
76
+ onPress={onClose}
77
+ accessibilityRole="button"
78
+ accessibilityLabel="Close"
79
+ style={({ pressed }) => [
80
+ styles.closeButton,
81
+ { backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainer },
82
+ pressed && styles.closePresseed,
83
+ ]}
84
+ >
85
+ <Ionicons
86
+ name="close"
87
+ size={20}
88
+ color={cr.surface.surface.sysOnSurface}
89
+ />
90
+ </Pressable>
91
+ </View>
92
+ </View>
93
+
94
+ {/* Content */}
95
+ {children && (
96
+ <ScrollView
97
+ style={styles.content}
98
+ contentContainerStyle={styles.contentInner}
99
+ showsVerticalScrollIndicator={false}
100
+ >
101
+ {children}
102
+ </ScrollView>
103
+ )}
104
+
105
+ {/* Actions */}
106
+ {(primaryAction || secondaryAction) && (
107
+ <View style={styles.actions}>
108
+ {primaryAction && (
109
+ <Button
110
+ label={primaryAction.label}
111
+ variant="filled"
112
+ size="xl"
113
+ fullWidth
114
+ onPress={primaryAction.onPress}
115
+ />
116
+ )}
117
+ {secondaryAction && (
118
+ <Button
119
+ label={secondaryAction.label}
120
+ variant="outlined"
121
+ size="xl"
122
+ fullWidth
123
+ onPress={secondaryAction.onPress}
124
+ />
125
+ )}
126
+ </View>
127
+ )}
128
+
129
+ </View>
130
+ </View>
131
+ </Modal>
132
+ );
133
+ }
134
+
135
+ const { colorRoles: cr2, dimensions: dim2 } = sys;
136
+
137
+ const styles = StyleSheet.create({
138
+ backdrop: {
139
+ ...StyleSheet.absoluteFillObject,
140
+ backgroundColor: 'rgba(0,0,0,0.4)',
141
+ },
142
+ sheetWrapper: {
143
+ flex: 1,
144
+ justifyContent: 'flex-end',
145
+ paddingHorizontal: dim2.spacing.padding.sysPadding6,
146
+ paddingBottom: dim2.spacing.padding.sysPadding6,
147
+ },
148
+ sheet: {
149
+ backgroundColor: cr2.surface.surfaceContainer.sysSurfaceContainerLowest,
150
+ borderRadius: dim2.borderRadius.sysRadiusXxl,
151
+ borderWidth: 1,
152
+ borderColor: 'rgba(255,255,255,0.4)',
153
+ overflow: 'hidden',
154
+ shadowColor: '#000',
155
+ shadowOffset: { width: 0, height: 4 },
156
+ shadowOpacity: 0.1,
157
+ shadowRadius: 16,
158
+ elevation: 8,
159
+ },
160
+ toolbar: {
161
+ alignItems: 'center',
162
+ width: '100%',
163
+ },
164
+ grabberRow: {
165
+ height: 16,
166
+ justifyContent: 'flex-end',
167
+ alignItems: 'center',
168
+ paddingBottom: 0,
169
+ paddingTop: 5,
170
+ },
171
+ grabber: {
172
+ width: 36,
173
+ height: 5,
174
+ borderRadius: 100,
175
+ backgroundColor: '#ccc',
176
+ },
177
+ titleRow: {
178
+ flexDirection: 'row',
179
+ alignItems: 'center',
180
+ justifyContent: 'space-between',
181
+ paddingHorizontal: dim2.spacing.padding.sysPadding16,
182
+ paddingVertical: dim2.spacing.padding.sysPadding8,
183
+ width: '100%',
184
+ },
185
+ titleText: {
186
+ flex: 1,
187
+ textAlign: 'center',
188
+ },
189
+ closeButton: {
190
+ width: 32,
191
+ height: 32,
192
+ borderRadius: dim2.borderRadius.sysRadiusFull,
193
+ alignItems: 'center',
194
+ justifyContent: 'center',
195
+ },
196
+ closePresseed: {
197
+ opacity: 0.7,
198
+ },
199
+ content: {
200
+ maxHeight: 400,
201
+ },
202
+ contentInner: {
203
+ padding: dim2.spacing.padding.sysPadding16,
204
+ },
205
+ actions: {
206
+ padding: dim2.spacing.padding.sysPadding24,
207
+ gap: dim2.spacing.padding.sysPadding12,
208
+ },
209
+ });
package/src/Card.tsx CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  StyleSheet,
7
7
  View,
8
8
  } from 'react-native';
9
+ import { Ionicons } from '@expo/vector-icons';
9
10
  import { sys } from './tokens';
10
11
  import { BodyText } from './BodyText';
11
12
 
@@ -29,58 +30,107 @@ export interface CardProps {
29
30
 
30
31
  const { colorRoles: cr, dimensions: dim } = sys;
31
32
 
32
- const VARIANT_TOKENS = {
33
+ // ── Variant tokens ────────────────────────────────────────────────────────────
34
+
35
+ const VARIANT_TOKENS: Record<
36
+ CardVariant,
37
+ {
38
+ bg: string;
39
+ borderColor: string;
40
+ borderWidth: number;
41
+ elevated: boolean;
42
+ titleColor: string;
43
+ descColor: string;
44
+ chevronColor: string;
45
+ // doubled inner gradient overlay color
46
+ innerBg?: string;
47
+ iconBadgeBg?: string;
48
+ iconBadgeColor?: string;
49
+ }
50
+ > = {
33
51
  outlined: {
34
- outerBg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
52
+ bg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
35
53
  borderColor: cr.outline.sysOutline,
36
54
  borderWidth: dim.borderWidth.sysStrokeThin,
37
- outerRadius: dim.borderRadius.sysRadiusLg,
38
55
  elevated: true,
39
56
  titleColor: cr.surface.surface.sysOnSurface,
40
57
  descColor: cr.surface.surface.sysOnSurfaceVariant,
58
+ chevronColor: cr.outline.sysOutlineFixed,
41
59
  },
42
60
  tonal: {
43
- outerBg: cr.addOn.primaryFixed.sysPrimaryFixedDim,
61
+ bg: cr.addOn.primaryFixed.sysPrimaryFixedDim,
44
62
  borderColor: 'transparent',
45
63
  borderWidth: 0,
46
- outerRadius: dim.borderRadius.sysRadiusLg,
47
64
  elevated: false,
48
65
  titleColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
49
66
  descColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
67
+ chevronColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
50
68
  },
51
69
  filled: {
52
- outerBg: cr.accent.primary.sysPrimaryContainer,
70
+ bg: cr.accent.primary.sysPrimary,
53
71
  borderColor: 'transparent',
54
72
  borderWidth: 0,
55
- outerRadius: dim.borderRadius.sysRadiusLg,
56
73
  elevated: true,
57
- titleColor: cr.surface.surface.sysOnSurface,
58
- descColor: cr.surface.surface.sysOnSurfaceVariant,
74
+ titleColor: cr.accent.primary.sysOnPrimary,
75
+ descColor: cr.accent.primary.sysOnPrimary,
76
+ chevronColor: cr.accent.primary.sysOnPrimary,
59
77
  },
60
78
  doubled: {
61
- outerBg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
79
+ bg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
62
80
  borderColor: cr.outline.sysOutline,
63
81
  borderWidth: dim.borderWidth.sysStrokeThin,
64
- outerRadius: dim.borderRadius.sysRadiusXl,
65
82
  elevated: false,
66
83
  titleColor: cr.surface.surface.sysOnSurface,
67
84
  descColor: cr.surface.surface.sysOnSurfaceVariant,
85
+ chevronColor: cr.outline.sysOutlineFixed,
86
+ innerBg: cr.transparent.primary.sysPrimary08,
87
+ iconBadgeBg: cr.accent.primary.sysPrimary,
88
+ iconBadgeColor: cr.accent.primary.sysOnPrimary,
68
89
  },
69
90
  image: {
70
- outerBg: 'transparent',
91
+ bg: 'transparent',
71
92
  borderColor: 'transparent',
72
93
  borderWidth: 0,
73
- outerRadius: dim.borderRadius.sysRadiusLg,
74
94
  elevated: false,
75
95
  titleColor: '#ffffff',
76
96
  descColor: cr.transparent.neutral.sysWhite80,
97
+ chevronColor: '#ffffff',
77
98
  },
78
99
  };
79
100
 
80
- const SIZE_PADDING: Record<CardSize, number> = {
81
- sm: dim.spacing.padding.sysPadding8,
82
- md: dim.spacing.padding.sysPadding12,
83
- lg: dim.spacing.padding.sysPadding16,
101
+ // ── Size tokens ───────────────────────────────────────────────────────────────
102
+
103
+ const SIZE_TOKENS = {
104
+ sm: {
105
+ layout: 'row' as const,
106
+ paddingH: dim.spacing.padding.sysPadding16,
107
+ paddingV: dim.spacing.padding.sysPadding12,
108
+ gap: dim.spacing.padding.sysPadding12,
109
+ borderRadius: dim.borderRadius.sysRadiusMd,
110
+ iconSize: 24,
111
+ titleVariant: 'medium' as const,
112
+ showChevron: true,
113
+ },
114
+ md: {
115
+ layout: 'column' as const,
116
+ paddingH: dim.spacing.padding.sysPadding16,
117
+ paddingV: dim.spacing.padding.sysPadding16,
118
+ gap: dim.spacing.padding.sysPadding24,
119
+ borderRadius: dim.borderRadius.sysRadiusLg,
120
+ iconSize: 32,
121
+ titleVariant: 'medium' as const,
122
+ showChevron: false,
123
+ },
124
+ lg: {
125
+ layout: 'column' as const,
126
+ paddingH: dim.spacing.padding.sysPadding16,
127
+ paddingV: dim.spacing.padding.sysPadding16,
128
+ gap: dim.spacing.padding.sysPadding32,
129
+ borderRadius: dim.borderRadius.sysRadiusLg,
130
+ iconSize: 32,
131
+ titleVariant: 'large' as const,
132
+ showChevron: false,
133
+ },
84
134
  };
85
135
 
86
136
  const ELEVATION = {
@@ -91,6 +141,8 @@ const ELEVATION = {
91
141
  elevation: 2,
92
142
  };
93
143
 
144
+ // ── Component ─────────────────────────────────────────────────────────────────
145
+
94
146
  export function Card({
95
147
  variant = 'outlined',
96
148
  size = 'lg',
@@ -106,30 +158,34 @@ export function Card({
106
158
  fullWidth = false,
107
159
  }: CardProps) {
108
160
  const v = VARIANT_TOKENS[variant];
109
- const padding = SIZE_PADDING[size];
161
+ const s = SIZE_TOKENS[size];
162
+ const isRow = s.layout === 'row';
110
163
 
111
164
  const outerStyle = [
112
- styles.root,
113
165
  {
114
- backgroundColor: v.outerBg,
166
+ backgroundColor: variant === 'doubled' ? v.bg : v.bg,
115
167
  borderColor: v.borderColor,
116
168
  borderWidth: v.borderWidth,
117
- borderRadius: v.outerRadius,
118
- padding: variant === 'doubled' ? dim.spacing.padding.sysPadding8 : padding,
169
+ borderRadius: variant === 'doubled' ? dim.borderRadius.sysRadiusXl : s.borderRadius,
170
+ paddingHorizontal: variant === 'doubled' ? dim.spacing.padding.sysPadding8 : s.paddingH,
171
+ paddingVertical: variant === 'doubled' ? dim.spacing.padding.sysPadding8 : s.paddingV,
119
172
  opacity: disabled ? 0.48 : 1,
120
173
  },
121
174
  v.elevated && ELEVATION,
122
175
  fullWidth && { alignSelf: 'stretch' as const },
176
+ isRow ? styles.rowRoot : styles.colRoot,
177
+ !isRow && { gap: s.gap },
123
178
  ];
124
179
 
180
+ // ── Text block ──────────────────────────────────────────────────────────────
125
181
  const textBlock = (title || description) ? (
126
- <View style={styles.textBlock}>
182
+ <View style={isRow ? styles.rowTextBlock : undefined}>
127
183
  {title && (
128
- <BodyText variant="large" color={v.titleColor}>
184
+ <BodyText variant={s.titleVariant} color={v.titleColor}>
129
185
  {title}
130
186
  </BodyText>
131
187
  )}
132
- {description && (
188
+ {!isRow && description && (
133
189
  <BodyText variant="small" color={v.descColor}>
134
190
  {description}
135
191
  </BodyText>
@@ -137,17 +193,20 @@ export function Card({
137
193
  </View>
138
194
  ) : null;
139
195
 
196
+ // ── Inner content by variant ────────────────────────────────────────────────
140
197
  let inner: React.ReactNode;
141
198
 
142
199
  if (variant === 'doubled') {
143
200
  inner = (
144
201
  <View
145
202
  style={[
146
- styles.doubledInner,
203
+ styles.colRoot,
147
204
  {
148
- backgroundColor: cr.transparent.primary.sysPrimary08,
149
- borderRadius: dim.borderRadius.sysRadiusLg,
150
- padding,
205
+ backgroundColor: v.innerBg,
206
+ borderRadius: s.borderRadius,
207
+ paddingHorizontal: s.paddingH,
208
+ paddingVertical: s.paddingV,
209
+ gap: s.gap,
151
210
  },
152
211
  ]}
153
212
  >
@@ -156,7 +215,7 @@ export function Card({
156
215
  style={[
157
216
  styles.doubledIconBadge,
158
217
  {
159
- backgroundColor: cr.accent.primary.sysPrimary,
218
+ backgroundColor: v.iconBadgeBg,
160
219
  borderRadius: dim.borderRadius.sysRadiusFull,
161
220
  },
162
221
  ]}
@@ -175,34 +234,53 @@ export function Card({
175
234
  <>
176
235
  <Image
177
236
  source={image}
178
- style={[StyleSheet.absoluteFillObject, { borderRadius: v.outerRadius }]}
237
+ style={[StyleSheet.absoluteFillObject, { borderRadius: s.borderRadius }]}
179
238
  resizeMode="cover"
180
239
  accessible={false}
181
240
  />
182
241
  <View
183
242
  style={[
184
243
  StyleSheet.absoluteFillObject,
185
- { borderRadius: v.outerRadius, backgroundColor: 'rgba(0,0,0,0.30)' },
244
+ { borderRadius: s.borderRadius, backgroundColor: 'rgba(0,0,0,0.30)' },
186
245
  ]}
187
246
  pointerEvents="none"
188
247
  />
189
248
  </>
190
249
  )}
191
- {icon && <View style={styles.iconSlot}>{icon}</View>}
250
+ {icon && <View style={{ width: s.iconSize, height: s.iconSize }}>{icon}</View>}
192
251
  {textBlock}
193
252
  {children}
194
253
  </>
195
254
  );
255
+ } else if (isRow) {
256
+ // sm — horizontal row
257
+ inner = (
258
+ <>
259
+ {icon && <View style={{ width: s.iconSize, height: s.iconSize }}>{icon}</View>}
260
+ {textBlock}
261
+ {s.showChevron && (
262
+ <Ionicons
263
+ name="chevron-forward"
264
+ size={20}
265
+ color={v.chevronColor}
266
+ accessible={false}
267
+ />
268
+ )}
269
+ {children}
270
+ </>
271
+ );
196
272
  } else {
273
+ // md / lg — vertical column
197
274
  inner = (
198
275
  <>
199
- {icon && <View style={styles.iconSlot}>{icon}</View>}
276
+ {icon && <View style={{ width: s.iconSize, height: s.iconSize }}>{icon}</View>}
200
277
  {textBlock}
201
278
  {children}
202
279
  </>
203
280
  );
204
281
  }
205
282
 
283
+ // ── Wrapper ─────────────────────────────────────────────────────────────────
206
284
  if (interactive || onPress) {
207
285
  return (
208
286
  <Pressable
@@ -210,33 +288,40 @@ export function Card({
210
288
  accessibilityRole="button"
211
289
  accessibilityLabel={accessibilityLabel ?? title}
212
290
  accessibilityState={{ disabled }}
213
- style={({ pressed }) => [outerStyle, pressed && !disabled && styles.pressed]}
291
+ style={({ pressed }) => [
292
+ styles.overflow,
293
+ ...outerStyle,
294
+ pressed && !disabled && styles.pressed,
295
+ ]}
214
296
  >
215
297
  {inner}
216
298
  </Pressable>
217
299
  );
218
300
  }
219
301
 
220
- return <View style={outerStyle}>{inner}</View>;
302
+ return (
303
+ <View style={[styles.overflow, ...outerStyle]}>
304
+ {inner}
305
+ </View>
306
+ );
221
307
  }
222
308
 
223
309
  const styles = StyleSheet.create({
224
- root: {
310
+ overflow: {
225
311
  overflow: 'hidden',
226
- gap: 32,
227
- },
228
- pressed: {
229
- opacity: 0.84,
230
312
  },
231
- iconSlot: {
232
- width: 32,
233
- height: 32,
313
+ colRoot: {
314
+ flexDirection: 'column',
315
+ alignItems: 'flex-start',
234
316
  },
235
- textBlock: {
236
- gap: 0,
317
+ rowRoot: {
318
+ flexDirection: 'row',
319
+ alignItems: 'center',
320
+ gap: 12,
237
321
  },
238
- doubledInner: {
239
- gap: 24,
322
+ rowTextBlock: {
323
+ flex: 1,
324
+ minWidth: 0,
240
325
  },
241
326
  doubledIconBadge: {
242
327
  width: 48,
@@ -244,4 +329,7 @@ const styles = StyleSheet.create({
244
329
  alignItems: 'center',
245
330
  justifyContent: 'center',
246
331
  },
332
+ pressed: {
333
+ opacity: 0.84,
334
+ },
247
335
  });
package/src/List.tsx ADDED
@@ -0,0 +1,179 @@
1
+ import React from 'react';
2
+ import { Pressable, StyleSheet, View } from 'react-native';
3
+ import { Ionicons } from '@expo/vector-icons';
4
+ import { sys } from './tokens';
5
+ import { BodyText } from './BodyText';
6
+
7
+ export type ListItemType = 'simple' | 'detailed';
8
+
9
+ export interface ListItemProps {
10
+ type?: ListItemType;
11
+ label: string;
12
+ subLabel?: string;
13
+ icon?: React.ReactNode;
14
+ onPress?: () => void;
15
+ disabled?: boolean;
16
+ showDivider?: boolean;
17
+ }
18
+
19
+ export interface ListProps {
20
+ items: Omit<ListItemProps, 'showDivider'>[];
21
+ type?: ListItemType;
22
+ }
23
+
24
+ const { colorRoles: cr, dimensions: dim } = sys;
25
+
26
+ // ── ListItem ──────────────────────────────────────────────────────────────────
27
+
28
+ export function ListItem({
29
+ type = 'simple',
30
+ label,
31
+ subLabel,
32
+ icon,
33
+ onPress,
34
+ disabled = false,
35
+ showDivider = true,
36
+ }: ListItemProps) {
37
+ const isDetailed = type === 'detailed';
38
+
39
+ const content = (
40
+ <View style={[styles.itemRow, { gap: dim.spacing.padding.sysPadding12 }]}>
41
+ {isDetailed ? (
42
+ // Detailed: icon inside circular badge
43
+ <View
44
+ style={[
45
+ styles.iconBadge,
46
+ { backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLow },
47
+ ]}
48
+ >
49
+ {icon}
50
+ </View>
51
+ ) : (
52
+ // Simple: bare icon
53
+ icon && <View style={styles.simpleIcon}>{icon}</View>
54
+ )}
55
+
56
+ <View style={styles.textBlock}>
57
+ <BodyText
58
+ variant="medium"
59
+ emphasized={isDetailed}
60
+ color={cr.surface.surface.sysOnSurface}
61
+ >
62
+ {label}
63
+ </BodyText>
64
+ {isDetailed && subLabel && (
65
+ <BodyText variant="small" color={cr.surface.surface.sysOnSurfaceVariant}>
66
+ {subLabel}
67
+ </BodyText>
68
+ )}
69
+ </View>
70
+
71
+ <Ionicons
72
+ name="chevron-forward"
73
+ size={20}
74
+ color={cr.outline.sysOutlineFixed}
75
+ accessible={false}
76
+ />
77
+ </View>
78
+ );
79
+
80
+ const divider = showDivider ? (
81
+ <View
82
+ style={[
83
+ styles.divider,
84
+ {
85
+ // Indent past icon + gap: 24px (simple) or 48px+16px (detailed)
86
+ marginLeft: isDetailed ? 64 : 36,
87
+ borderBottomColor: cr.outline.sysOutline,
88
+ },
89
+ ]}
90
+ />
91
+ ) : null;
92
+
93
+ if (onPress) {
94
+ return (
95
+ <View style={disabled && styles.disabled}>
96
+ <Pressable
97
+ onPress={disabled ? undefined : onPress}
98
+ accessibilityRole="button"
99
+ accessibilityLabel={label}
100
+ accessibilityState={{ disabled }}
101
+ style={({ pressed }) => [
102
+ isDetailed
103
+ ? { paddingTop: dim.spacing.padding.sysPadding12 }
104
+ : {
105
+ paddingVertical: dim.spacing.padding.sysPadding16,
106
+ },
107
+ pressed && !disabled && styles.pressed,
108
+ ]}
109
+ >
110
+ {content}
111
+ </Pressable>
112
+ {divider}
113
+ </View>
114
+ );
115
+ }
116
+
117
+ return (
118
+ <View style={disabled && styles.disabled}>
119
+ <View
120
+ style={
121
+ isDetailed
122
+ ? { paddingTop: dim.spacing.padding.sysPadding12 }
123
+ : { paddingVertical: dim.spacing.padding.sysPadding16 }
124
+ }
125
+ >
126
+ {content}
127
+ </View>
128
+ {divider}
129
+ </View>
130
+ );
131
+ }
132
+
133
+ // ── List ──────────────────────────────────────────────────────────────────────
134
+
135
+ export function List({ items, type = 'simple' }: ListProps) {
136
+ return (
137
+ <View>
138
+ {items.map((item, index) => (
139
+ <ListItem
140
+ key={index}
141
+ {...item}
142
+ type={item.type ?? type}
143
+ showDivider={index < items.length - 1}
144
+ />
145
+ ))}
146
+ </View>
147
+ );
148
+ }
149
+
150
+ const styles = StyleSheet.create({
151
+ itemRow: {
152
+ flexDirection: 'row',
153
+ alignItems: 'center',
154
+ },
155
+ simpleIcon: {
156
+ width: 24,
157
+ height: 24,
158
+ },
159
+ iconBadge: {
160
+ width: 48,
161
+ height: 48,
162
+ borderRadius: 999,
163
+ alignItems: 'center',
164
+ justifyContent: 'center',
165
+ },
166
+ textBlock: {
167
+ flex: 1,
168
+ minWidth: 0,
169
+ },
170
+ divider: {
171
+ borderBottomWidth: StyleSheet.hairlineWidth,
172
+ },
173
+ pressed: {
174
+ backgroundColor: 'rgba(0,0,0,0.04)',
175
+ },
176
+ disabled: {
177
+ opacity: 0.48,
178
+ },
179
+ });
@@ -0,0 +1,312 @@
1
+ import React, { useRef } from 'react';
2
+ import {
3
+ Animated,
4
+ LayoutAnimation,
5
+ Platform,
6
+ Pressable,
7
+ StyleSheet,
8
+ UIManager,
9
+ View,
10
+ } from 'react-native';
11
+ import { Ionicons } from '@expo/vector-icons';
12
+ import { sys } from './tokens';
13
+ import { BodyText } from './BodyText';
14
+
15
+ if (Platform.OS === 'android') {
16
+ UIManager.setLayoutAnimationEnabledExperimental?.(true);
17
+ }
18
+
19
+ // ─── Dropdown Item ───────────────────────────────────────────────────────────
20
+
21
+ export interface PlanCardItemData {
22
+ id: string;
23
+ title: string;
24
+ subtitle?: string;
25
+ checked?: boolean;
26
+ onToggle?: () => void;
27
+ onAudio?: () => void;
28
+ onOpen?: () => void;
29
+ }
30
+
31
+ interface DropdownItemProps extends PlanCardItemData {
32
+ showDivider?: boolean;
33
+ }
34
+
35
+ const { colorRoles: cr, dimensions: dim } = sys;
36
+
37
+ export function PlanCardDropdownItem({
38
+ title,
39
+ subtitle,
40
+ checked = false,
41
+ onToggle,
42
+ onAudio,
43
+ onOpen,
44
+ showDivider = true,
45
+ }: DropdownItemProps) {
46
+ return (
47
+ <View>
48
+ <View style={itemStyles.row}>
49
+ {/* Check indicator */}
50
+ <Pressable
51
+ onPress={onToggle}
52
+ accessibilityRole="checkbox"
53
+ accessibilityState={{ checked }}
54
+ style={itemStyles.checkWrap}
55
+ >
56
+ {checked ? (
57
+ <View style={itemStyles.checkedCircle}>
58
+ <Ionicons name="checkmark" size={12} color="#fff" />
59
+ </View>
60
+ ) : (
61
+ <View style={itemStyles.uncheckedCircle} />
62
+ )}
63
+ </Pressable>
64
+
65
+ {/* Text */}
66
+ <View style={itemStyles.textCol}>
67
+ <BodyText variant="small" color={cr.surface.surface.sysOnSurface}>
68
+ {title}
69
+ </BodyText>
70
+ {subtitle && (
71
+ <BodyText variant="small" color={cr.surface.surface.sysOnSurfaceVariant}>
72
+ {subtitle}
73
+ </BodyText>
74
+ )}
75
+ </View>
76
+
77
+ {/* Action buttons */}
78
+ <View style={itemStyles.actions}>
79
+ {onAudio && (
80
+ <Pressable
81
+ onPress={onAudio}
82
+ accessibilityRole="button"
83
+ accessibilityLabel="Listen"
84
+ style={({ pressed }) => [itemStyles.actionBtn, pressed && itemStyles.pressed]}
85
+ >
86
+ <Ionicons name="headset-outline" size={16} color={cr.surface.surface.sysOnSurface} />
87
+ </Pressable>
88
+ )}
89
+ {onOpen && (
90
+ <Pressable
91
+ onPress={onOpen}
92
+ accessibilityRole="button"
93
+ accessibilityLabel="Open"
94
+ style={({ pressed }) => [itemStyles.actionBtn, pressed && itemStyles.pressed]}
95
+ >
96
+ <Ionicons name="arrow-up-circle-outline" size={16} color={cr.surface.surface.sysOnSurface} />
97
+ </Pressable>
98
+ )}
99
+ </View>
100
+ </View>
101
+
102
+ {showDivider && <View style={itemStyles.divider} />}
103
+ </View>
104
+ );
105
+ }
106
+
107
+ const itemStyles = StyleSheet.create({
108
+ row: {
109
+ flexDirection: 'row',
110
+ alignItems: 'center',
111
+ paddingVertical: dim.spacing.padding.sysPadding16,
112
+ gap: dim.spacing.padding.sysPadding12,
113
+ },
114
+ checkWrap: {
115
+ width: 20,
116
+ height: 20,
117
+ alignItems: 'center',
118
+ justifyContent: 'center',
119
+ },
120
+ checkedCircle: {
121
+ width: 20,
122
+ height: 20,
123
+ borderRadius: dim.borderRadius.sysRadiusFull,
124
+ backgroundColor: cr.custom.success.sysSuccess,
125
+ alignItems: 'center',
126
+ justifyContent: 'center',
127
+ },
128
+ uncheckedCircle: {
129
+ width: 20,
130
+ height: 20,
131
+ borderRadius: dim.borderRadius.sysRadiusFull,
132
+ borderWidth: 1.5,
133
+ borderColor: cr.outline.sysOutlineFixed,
134
+ },
135
+ textCol: {
136
+ flex: 1,
137
+ gap: 2,
138
+ },
139
+ actions: {
140
+ flexDirection: 'row',
141
+ gap: dim.spacing.padding.sysPadding8,
142
+ },
143
+ actionBtn: {
144
+ width: 32,
145
+ height: 32,
146
+ borderRadius: dim.borderRadius.sysRadiusFull,
147
+ borderWidth: dim.borderWidth.sysStrokeThin,
148
+ borderColor: cr.outline.sysOutline,
149
+ alignItems: 'center',
150
+ justifyContent: 'center',
151
+ },
152
+ pressed: {
153
+ opacity: 0.7,
154
+ },
155
+ divider: {
156
+ height: StyleSheet.hairlineWidth,
157
+ backgroundColor: cr.outline.sysOutline,
158
+ },
159
+ });
160
+
161
+ // ─── Plan Card ────────────────────────────────────────────────────────────────
162
+
163
+ export interface PlanCardProps {
164
+ title: string;
165
+ icon?: React.ReactNode;
166
+ tag?: string;
167
+ items?: PlanCardItemData[];
168
+ expanded?: boolean;
169
+ onToggle?: () => void;
170
+ children?: React.ReactNode;
171
+ }
172
+
173
+ export function PlanCard({
174
+ title,
175
+ icon,
176
+ tag,
177
+ items,
178
+ expanded = false,
179
+ onToggle,
180
+ children,
181
+ }: PlanCardProps) {
182
+ function handleToggle() {
183
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
184
+ onToggle?.();
185
+ }
186
+
187
+ const hasContent = expanded && (children || (items && items.length > 0));
188
+
189
+ return (
190
+ <View style={cardStyles.card}>
191
+ {/* Header */}
192
+ <View style={cardStyles.header}>
193
+ {/* Left: icon + title stacked */}
194
+ <View style={cardStyles.left}>
195
+ <View style={cardStyles.iconCircle}>
196
+ {icon ?? (
197
+ <Ionicons name="document-text-outline" size={16} color={cr.accent.primary.sysOnPrimary} />
198
+ )}
199
+ </View>
200
+ <BodyText
201
+ variant="medium"
202
+ color={cr.surface.surface.sysOnSurface}
203
+ >
204
+ {title}
205
+ </BodyText>
206
+ </View>
207
+
208
+ {/* Right: tag chip + toggle */}
209
+ <View style={cardStyles.right}>
210
+ {tag && (
211
+ <View style={cardStyles.chip}>
212
+ <BodyText variant="small" color={cr.custom.info.sysOnInfoContainer}>
213
+ {tag}
214
+ </BodyText>
215
+ </View>
216
+ )}
217
+ <Pressable
218
+ onPress={handleToggle}
219
+ accessibilityRole="button"
220
+ accessibilityLabel={expanded ? 'Collapse' : 'Expand'}
221
+ style={({ pressed }) => [
222
+ cardStyles.toggleBtn,
223
+ pressed && cardStyles.pressed,
224
+ ]}
225
+ >
226
+ <Ionicons
227
+ name={expanded ? 'remove' : 'add'}
228
+ size={16}
229
+ color={cr.surface.surface.sysOnSurfaceVariant}
230
+ />
231
+ </Pressable>
232
+ </View>
233
+ </View>
234
+
235
+ {/* Expanded content */}
236
+ {hasContent && (
237
+ <View style={cardStyles.content}>
238
+ <View style={cardStyles.contentDivider} />
239
+ {children
240
+ ? children
241
+ : items!.map((item, index) => (
242
+ <PlanCardDropdownItem
243
+ key={item.id}
244
+ {...item}
245
+ showDivider={index < items!.length - 1}
246
+ />
247
+ ))}
248
+ </View>
249
+ )}
250
+ </View>
251
+ );
252
+ }
253
+
254
+ const cardStyles = StyleSheet.create({
255
+ card: {
256
+ backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
257
+ borderRadius: dim.borderRadius.sysRadiusMd,
258
+ borderWidth: dim.borderWidth.sysStrokeThin,
259
+ borderColor: cr.outline.sysOutline,
260
+ padding: dim.spacing.padding.sysPadding16,
261
+ overflow: 'hidden',
262
+ },
263
+ header: {
264
+ flexDirection: 'row',
265
+ alignItems: 'center',
266
+ justifyContent: 'space-between',
267
+ gap: dim.spacing.padding.sysPadding12,
268
+ },
269
+ left: {
270
+ flex: 1,
271
+ gap: dim.spacing.padding.sysPadding8,
272
+ },
273
+ iconCircle: {
274
+ width: 28,
275
+ height: 28,
276
+ borderRadius: dim.borderRadius.sysRadiusFull,
277
+ backgroundColor: cr.accent.primary.sysPrimary,
278
+ alignItems: 'center',
279
+ justifyContent: 'center',
280
+ },
281
+ right: {
282
+ flexDirection: 'row',
283
+ alignItems: 'center',
284
+ gap: dim.spacing.padding.sysPadding8,
285
+ alignSelf: 'flex-start',
286
+ },
287
+ chip: {
288
+ backgroundColor: cr.custom.info.sysInfoContainer,
289
+ borderRadius: dim.borderRadius.sysRadiusFull,
290
+ paddingHorizontal: dim.spacing.padding.sysPadding8,
291
+ paddingVertical: dim.spacing.padding.sysPadding4,
292
+ },
293
+ toggleBtn: {
294
+ width: 20,
295
+ height: 20,
296
+ borderRadius: dim.borderRadius.sysRadiusFull,
297
+ backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainer,
298
+ alignItems: 'center',
299
+ justifyContent: 'center',
300
+ },
301
+ pressed: {
302
+ opacity: 0.7,
303
+ },
304
+ content: {
305
+ marginTop: dim.spacing.padding.sysPadding8,
306
+ },
307
+ contentDivider: {
308
+ height: StyleSheet.hairlineWidth,
309
+ backgroundColor: cr.outline.sysOutline,
310
+ marginBottom: dim.spacing.padding.sysPadding4,
311
+ },
312
+ });
@@ -0,0 +1,114 @@
1
+ import React from 'react';
2
+ import { Pressable, StyleSheet, View } from 'react-native';
3
+ import { sys } from './tokens';
4
+ import { BodyText } from './BodyText';
5
+
6
+ export interface SegmentedControlOption {
7
+ value: string;
8
+ label: string;
9
+ icon?: React.ReactNode;
10
+ disabled?: boolean;
11
+ }
12
+
13
+ export interface SegmentedControlProps {
14
+ options: SegmentedControlOption[];
15
+ value: string;
16
+ onChange: (value: string) => void;
17
+ fullWidth?: boolean;
18
+ }
19
+
20
+ const { colorRoles: cr, dimensions: dim } = sys;
21
+
22
+ export function SegmentedControl({
23
+ options,
24
+ value,
25
+ onChange,
26
+ fullWidth = false,
27
+ }: SegmentedControlProps) {
28
+ return (
29
+ <View style={[styles.container, fullWidth && styles.fullWidth]}>
30
+ {options.map((option) => {
31
+ const isActive = option.value === value;
32
+ const isDisabled = option.disabled ?? false;
33
+
34
+ return (
35
+ <Pressable
36
+ key={option.value}
37
+ onPress={isDisabled ? undefined : () => onChange(option.value)}
38
+ accessibilityRole="tab"
39
+ accessibilityLabel={option.label}
40
+ accessibilityState={{ selected: isActive, disabled: isDisabled }}
41
+ style={({ pressed }) => [
42
+ styles.item,
43
+ {
44
+ paddingHorizontal: dim.spacing.padding.sysPadding12,
45
+ paddingVertical: dim.spacing.padding.sysPadding6,
46
+ gap: dim.spacing.padding.sysPadding4,
47
+ borderRadius: dim.borderRadius.sysRadiusFull,
48
+ },
49
+ isActive && { backgroundColor: cr.accent.primary.sysPrimary },
50
+ !isActive && pressed && !isDisabled && styles.pressed,
51
+ isDisabled && styles.disabled,
52
+ ]}
53
+ >
54
+ {option.icon && (
55
+ <View style={styles.iconSlot}>{option.icon}</View>
56
+ )}
57
+ <BodyText
58
+ variant="small"
59
+ emphasized={isActive}
60
+ color={
61
+ isActive
62
+ ? cr.accent.primary.sysOnPrimary
63
+ : cr.surface.surface.sysOnSurfaceVariant
64
+ }
65
+ numberOfLines={1}
66
+ >
67
+ {option.label}
68
+ </BodyText>
69
+ </Pressable>
70
+ );
71
+ })}
72
+ </View>
73
+ );
74
+ }
75
+
76
+ const styles = StyleSheet.create({
77
+ container: {
78
+ flexDirection: 'row',
79
+ alignItems: 'center',
80
+ alignSelf: 'flex-start',
81
+ backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
82
+ borderWidth: dim.borderWidth.sysStrokeThin,
83
+ borderColor: cr.outline.sysOutline,
84
+ borderRadius: dim.borderRadius.sysRadiusFull,
85
+ padding: dim.spacing.padding.sysPadding4,
86
+ gap: dim.spacing.padding.sysPadding4,
87
+ shadowColor: '#000',
88
+ shadowOffset: { width: 0, height: 2 },
89
+ shadowOpacity: 0.06,
90
+ shadowRadius: 4,
91
+ elevation: 1,
92
+ },
93
+ fullWidth: {
94
+ alignSelf: 'stretch',
95
+ },
96
+ item: {
97
+ flex: 1,
98
+ flexDirection: 'row',
99
+ alignItems: 'center',
100
+ justifyContent: 'center',
101
+ },
102
+ iconSlot: {
103
+ width: 16,
104
+ height: 16,
105
+ alignItems: 'center',
106
+ justifyContent: 'center',
107
+ },
108
+ pressed: {
109
+ backgroundColor: cr.transparent.neutral.sysBlack10,
110
+ },
111
+ disabled: {
112
+ opacity: 0.48,
113
+ },
114
+ });
@@ -0,0 +1,189 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import {
3
+ Animated,
4
+ Pressable,
5
+ StyleSheet,
6
+ View,
7
+ } from 'react-native';
8
+ import { Ionicons } from '@expo/vector-icons';
9
+ import { sys } from './tokens';
10
+ import { BodyText } from './BodyText';
11
+
12
+ export type SnackbarVariant = 'filled' | 'outlined';
13
+
14
+ export interface SnackbarProps {
15
+ visible: boolean;
16
+ message: string;
17
+ variant?: SnackbarVariant;
18
+ actionLabel?: string;
19
+ onAction?: () => void;
20
+ onClose?: () => void;
21
+ }
22
+
23
+ const { colorRoles: cr, dimensions: dim } = sys;
24
+
25
+ export function Snackbar({
26
+ visible,
27
+ message,
28
+ variant = 'filled',
29
+ actionLabel,
30
+ onAction,
31
+ onClose,
32
+ }: SnackbarProps) {
33
+ const translateY = useRef(new Animated.Value(100)).current;
34
+ const opacity = useRef(new Animated.Value(0)).current;
35
+
36
+ useEffect(() => {
37
+ if (visible) {
38
+ Animated.parallel([
39
+ Animated.spring(translateY, {
40
+ toValue: 0,
41
+ useNativeDriver: true,
42
+ damping: 20,
43
+ stiffness: 300,
44
+ }),
45
+ Animated.timing(opacity, {
46
+ toValue: 1,
47
+ duration: 200,
48
+ useNativeDriver: true,
49
+ }),
50
+ ]).start();
51
+ } else {
52
+ Animated.parallel([
53
+ Animated.timing(translateY, {
54
+ toValue: 100,
55
+ duration: 200,
56
+ useNativeDriver: true,
57
+ }),
58
+ Animated.timing(opacity, {
59
+ toValue: 0,
60
+ duration: 150,
61
+ useNativeDriver: true,
62
+ }),
63
+ ]).start();
64
+ }
65
+ }, [visible]);
66
+
67
+ const isFilled = variant === 'filled';
68
+
69
+ const containerStyle = isFilled ? styles.containerFilled : styles.containerOutlined;
70
+ const labelColor = isFilled
71
+ ? cr.accent.primary.sysOnPrimary
72
+ : cr.surface.surface.sysOnSurface;
73
+ const actionColor = isFilled
74
+ ? cr.accent.primary.sysOnPrimary
75
+ : cr.accent.primary.sysPrimary;
76
+ const closeBg = isFilled
77
+ ? cr.transparent.neutral.sysWhite10
78
+ : cr.surface.surfaceContainer.sysSurfaceContainer;
79
+ const closeIconColor = isFilled
80
+ ? cr.accent.primary.sysOnPrimary
81
+ : cr.surface.surface.sysOnSurfaceVariant;
82
+
83
+ return (
84
+ <Animated.View
85
+ style={[
86
+ styles.wrapper,
87
+ { opacity, transform: [{ translateY }] },
88
+ ]}
89
+ pointerEvents={visible ? 'box-none' : 'none'}
90
+ >
91
+ <View style={[styles.container, containerStyle]}>
92
+ <BodyText
93
+ variant="medium"
94
+ color={labelColor}
95
+ style={styles.label}
96
+ numberOfLines={2}
97
+ >
98
+ {message}
99
+ </BodyText>
100
+
101
+ <View style={styles.actionSlot}>
102
+ {actionLabel && onAction && (
103
+ <Pressable
104
+ onPress={onAction}
105
+ accessibilityRole="button"
106
+ accessibilityLabel={actionLabel}
107
+ style={({ pressed }) => [
108
+ styles.actionButton,
109
+ pressed && styles.pressed,
110
+ ]}
111
+ >
112
+ <BodyText variant="small" emphasized color={actionColor}>
113
+ {actionLabel}
114
+ </BodyText>
115
+ </Pressable>
116
+ )}
117
+
118
+ {onClose && (
119
+ <Pressable
120
+ onPress={onClose}
121
+ accessibilityRole="button"
122
+ accessibilityLabel="Dismiss"
123
+ style={({ pressed }) => [
124
+ styles.closeButton,
125
+ { backgroundColor: closeBg },
126
+ pressed && styles.pressed,
127
+ ]}
128
+ >
129
+ <Ionicons name="close" size={16} color={closeIconColor} />
130
+ </Pressable>
131
+ )}
132
+ </View>
133
+ </View>
134
+ </Animated.View>
135
+ );
136
+ }
137
+
138
+ const styles = StyleSheet.create({
139
+ wrapper: {
140
+ position: 'absolute',
141
+ bottom: dim.spacing.padding.sysPadding24,
142
+ left: dim.spacing.padding.sysPadding16,
143
+ right: dim.spacing.padding.sysPadding16,
144
+ alignItems: 'center',
145
+ },
146
+ container: {
147
+ flexDirection: 'row',
148
+ alignItems: 'center',
149
+ paddingVertical: dim.spacing.padding.sysPadding12,
150
+ paddingHorizontal: dim.spacing.padding.sysPadding16,
151
+ borderRadius: dim.borderRadius.sysRadiusMd,
152
+ gap: dim.spacing.padding.sysPadding16,
153
+ width: '100%',
154
+ shadowColor: '#000',
155
+ shadowOffset: { width: 0, height: 4 },
156
+ shadowOpacity: 0.12,
157
+ shadowRadius: 12,
158
+ elevation: 6,
159
+ },
160
+ containerFilled: {
161
+ backgroundColor: cr.accent.primary.sysPrimaryContainer,
162
+ },
163
+ containerOutlined: {
164
+ backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
165
+ borderWidth: dim.borderWidth.sysStrokeThin,
166
+ borderColor: cr.outline.sysOutline,
167
+ },
168
+ label: {
169
+ flex: 1,
170
+ },
171
+ actionSlot: {
172
+ flexDirection: 'row',
173
+ alignItems: 'center',
174
+ gap: dim.spacing.padding.sysPadding12,
175
+ },
176
+ actionButton: {
177
+ paddingVertical: dim.spacing.padding.sysPadding4,
178
+ },
179
+ closeButton: {
180
+ width: 24,
181
+ height: 24,
182
+ borderRadius: dim.borderRadius.sysRadiusFull,
183
+ alignItems: 'center',
184
+ justifyContent: 'center',
185
+ },
186
+ pressed: {
187
+ opacity: 0.7,
188
+ },
189
+ });
package/src/index.ts CHANGED
@@ -31,9 +31,15 @@ export type { DividerProps, DividerVariant, DividerWeight } from './Divider';
31
31
  export { EmptyState } from './EmptyState';
32
32
  export type { EmptyStateProps, EmptyStateStyle, EmptyStateViewport } from './EmptyState';
33
33
 
34
+ export { List, ListItem } from './List';
35
+ export type { ListProps, ListItemProps, ListItemType } from './List';
36
+
34
37
  export { Input } from './Input';
35
38
  export type { InputProps, InputSize } from './Input';
36
39
 
40
+ export { PlanCard, PlanCardDropdownItem } from './PlanCard';
41
+ export type { PlanCardProps, PlanCardItemData } from './PlanCard';
42
+
37
43
  export { Pagination } from './Pagination';
38
44
  export type { PaginationProps, PaginationSize } from './Pagination';
39
45
 
@@ -43,15 +49,24 @@ export type { ProgressTrackerProps, ProgressTrackerSize, ProgressBarProps, Track
43
49
  export { RadioButton } from './RadioButton';
44
50
  export type { RadioButtonProps, RadioButtonSize } from './RadioButton';
45
51
 
52
+ export { SegmentedControl } from './SegmentedControl';
53
+ export type { SegmentedControlProps, SegmentedControlOption } from './SegmentedControl';
54
+
46
55
  export { Slider } from './Slider';
47
56
  export type { SliderProps } from './Slider';
48
57
 
58
+ export { Snackbar } from './Snackbar';
59
+ export type { SnackbarProps, SnackbarVariant } from './Snackbar';
60
+
49
61
  export { Switch } from './Switch';
50
62
  export type { SwitchProps } from './Switch';
51
63
 
52
64
  export { Tooltip } from './Tooltip';
53
65
  export type { TooltipProps, TooltipVariant, TooltipDirection } from './Tooltip';
54
66
 
67
+ export { ActionSheet } from './ActionSheet';
68
+ export type { ActionSheetProps, ActionSheetAction } from './ActionSheet';
69
+
55
70
  export { BodyText } from './BodyText';
56
71
  export type { BodyTextProps, BodyVariant } from './BodyText';
57
72
 
package/src/tokens.ts CHANGED
@@ -88,6 +88,7 @@ export const sys = {
88
88
  sysRadiusMd: 12,
89
89
  sysRadiusLg: 16,
90
90
  sysRadiusXl: 24,
91
+ sysRadiusXxl: 32,
91
92
  sysRadiusFull: 999,
92
93
  },
93
94
  borderWidth: {
@@ -100,6 +101,7 @@ export const sys = {
100
101
  padding: {
101
102
  sysPadding2: 2,
102
103
  sysPadding4: 4,
104
+ sysPadding6: 6,
103
105
  sysPadding8: 8,
104
106
  sysPadding12: 12,
105
107
  sysPadding16: 16,