@compsych/mobile-ui 1.0.10 → 1.0.13
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 +1 -1
- package/src/components/ActionSheet/index.tsx +124 -57
- package/src/components/Alert/index.tsx +19 -10
- package/src/components/Avatar/index.tsx +95 -92
- package/src/components/Badge/index.tsx +39 -36
- package/src/components/BodyText/index.tsx +5 -5
- package/src/components/Breadcrumb/index.tsx +32 -30
- package/src/components/Button/Button.test.tsx +13 -7
- package/src/components/Button/index.tsx +114 -92
- package/src/components/ChatBubble/ChatBubble.test.tsx +77 -0
- package/src/components/ChatBubble/index.tsx +187 -0
- package/src/components/ChatInput/ChatInput.test.tsx +90 -0
- package/src/components/ChatInput/index.tsx +263 -0
- package/src/components/Checkbox/index.tsx +24 -21
- package/src/components/Chip/index.tsx +75 -72
- package/src/components/Divider/index.tsx +3 -3
- package/src/components/EmptyState/index.tsx +72 -55
- package/src/components/HeaderText/index.tsx +5 -5
- package/src/components/Input/index.tsx +40 -37
- package/src/components/List/index.tsx +2 -3
- package/src/components/Pagination/index.tsx +56 -43
- package/src/components/PlanCard/index.tsx +118 -50
- package/src/components/ProgressTracker/index.tsx +44 -26
- package/src/components/PromotionCard/index.tsx +217 -212
- package/src/components/RadioButton/index.tsx +27 -25
- package/src/components/ScreenContainer/ScreenContainer.test.tsx +12 -2
- package/src/components/ScreenContainer/index.tsx +22 -12
- package/src/components/SegmentedControl/index.tsx +37 -30
- package/src/components/SelectionCard/index.tsx +38 -26
- package/src/components/ServiceCard/index.tsx +162 -130
- package/src/components/Slider/index.tsx +77 -77
- package/src/components/Snackbar/index.tsx +69 -57
- package/src/components/Switch/index.tsx +13 -12
- package/src/components/Tooltip/index.tsx +3 -3
- package/src/index.ts +13 -0
- package/src/theme/ThemeContext.ts +11 -0
- package/src/theme/ThemeProvider.tsx +40 -0
- package/src/theme/__tests__/ThemeProvider.test.tsx +101 -0
- package/src/theme/__tests__/defaultTheme.test.ts +36 -0
- package/src/theme/__tests__/mergeTheme.test.ts +94 -0
- package/src/theme/defaultTheme.ts +15 -0
- package/src/theme/index.ts +8 -0
- package/src/theme/mergeTheme.ts +47 -0
- package/src/theme/types.ts +39 -0
- package/src/theme/useTheme.ts +16 -0
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
2
8
|
|
|
3
9
|
import {
|
|
4
10
|
LayoutChangeEvent,
|
|
@@ -10,7 +16,7 @@ import {
|
|
|
10
16
|
ViewStyle,
|
|
11
17
|
} from 'react-native';
|
|
12
18
|
|
|
13
|
-
import {
|
|
19
|
+
import { useTheme } from '../../theme';
|
|
14
20
|
|
|
15
21
|
export interface SliderProps {
|
|
16
22
|
/** Controlled value */
|
|
@@ -34,8 +40,6 @@ export interface SliderProps {
|
|
|
34
40
|
style?: StyleProp<ViewStyle>;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
|
|
38
|
-
|
|
39
43
|
// ── Layout constants ──────────────────────────────────────────────────────────
|
|
40
44
|
const TRACK_HEIGHT = 8;
|
|
41
45
|
const THUMB_SIZE = 24;
|
|
@@ -67,6 +71,8 @@ export function Slider({
|
|
|
67
71
|
accessibilityLabel,
|
|
68
72
|
style,
|
|
69
73
|
}: SliderProps) {
|
|
74
|
+
const { colorRoles: cr, dimensions: dim, typeScale: ts } = useTheme();
|
|
75
|
+
|
|
70
76
|
const isControlled = valueProp !== undefined;
|
|
71
77
|
const [internalValue, setInternalValue] = useState<number>(
|
|
72
78
|
defaultValue ?? min,
|
|
@@ -181,10 +187,69 @@ export function Slider({
|
|
|
181
187
|
elevation: 1,
|
|
182
188
|
};
|
|
183
189
|
|
|
190
|
+
// ── Token-derived styles ──────────────────────────────────────────────────────
|
|
191
|
+
const tokenStyles = useMemo(
|
|
192
|
+
() => ({
|
|
193
|
+
root: {
|
|
194
|
+
gap: dim.spacing.padding.sysPadding8,
|
|
195
|
+
alignSelf: 'stretch' as const,
|
|
196
|
+
},
|
|
197
|
+
label: {
|
|
198
|
+
color: cr.surface.surface.sysOnSurface,
|
|
199
|
+
fontSize: ts.labelMedium.sysFontSize,
|
|
200
|
+
lineHeight: ts.labelMedium.sysLineHeight,
|
|
201
|
+
letterSpacing: ts.labelMedium.sysTracking,
|
|
202
|
+
fontWeight: '400' as const,
|
|
203
|
+
includeFontPadding: false,
|
|
204
|
+
},
|
|
205
|
+
row: {
|
|
206
|
+
flexDirection: 'row' as const,
|
|
207
|
+
alignItems: 'center' as const,
|
|
208
|
+
gap: dim.spacing.padding.sysPadding8,
|
|
209
|
+
},
|
|
210
|
+
rangeLabel: {
|
|
211
|
+
color: cr.surface.surface.sysOnSurfaceVariant,
|
|
212
|
+
fontSize: ts.bodySmall.sysFontSize,
|
|
213
|
+
lineHeight: ts.bodySmall.sysLineHeight,
|
|
214
|
+
fontWeight: '400' as const,
|
|
215
|
+
includeFontPadding: false,
|
|
216
|
+
flexShrink: 0,
|
|
217
|
+
},
|
|
218
|
+
trackBg: {
|
|
219
|
+
position: 'absolute' as const,
|
|
220
|
+
top: TRACK_TOP,
|
|
221
|
+
left: 0,
|
|
222
|
+
right: 0,
|
|
223
|
+
height: TRACK_HEIGHT,
|
|
224
|
+
borderRadius: 9999,
|
|
225
|
+
backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerHighest,
|
|
226
|
+
},
|
|
227
|
+
trackFill: {
|
|
228
|
+
position: 'absolute' as const,
|
|
229
|
+
top: TRACK_TOP,
|
|
230
|
+
left: 0,
|
|
231
|
+
height: TRACK_HEIGHT,
|
|
232
|
+
borderRadius: 9999,
|
|
233
|
+
backgroundColor: cr.accent.primary.sysPrimary,
|
|
234
|
+
},
|
|
235
|
+
thumb: {
|
|
236
|
+
position: 'absolute' as const,
|
|
237
|
+
top: THUMB_TOP,
|
|
238
|
+
width: THUMB_SIZE,
|
|
239
|
+
height: THUMB_SIZE,
|
|
240
|
+
borderRadius: 9999,
|
|
241
|
+
backgroundColor: cr.surface.surface.sysSurface,
|
|
242
|
+
alignItems: 'center' as const,
|
|
243
|
+
justifyContent: 'center' as const,
|
|
244
|
+
},
|
|
245
|
+
}),
|
|
246
|
+
[cr, dim, ts],
|
|
247
|
+
);
|
|
248
|
+
|
|
184
249
|
// ── Render ────────────────────────────────────────────────────────────────────
|
|
185
250
|
return (
|
|
186
251
|
<View
|
|
187
|
-
style={[
|
|
252
|
+
style={[tokenStyles.root, style]}
|
|
188
253
|
accessible
|
|
189
254
|
accessibilityRole="adjustable"
|
|
190
255
|
accessibilityLabel={accessibilityLabel ?? label}
|
|
@@ -202,12 +267,12 @@ export function Slider({
|
|
|
202
267
|
onBlur={() => setFocused(false)}
|
|
203
268
|
>
|
|
204
269
|
{/* ── Optional label ────────────────────────────────────────────────── */}
|
|
205
|
-
{label && <Text style={
|
|
270
|
+
{label && <Text style={tokenStyles.label}>{label}</Text>}
|
|
206
271
|
|
|
207
272
|
{/* ── Slider row ────────────────────────────────────────────────────── */}
|
|
208
|
-
<View style={[
|
|
273
|
+
<View style={[tokenStyles.row, disabled && styles.rowDisabled]}>
|
|
209
274
|
{/* Min label */}
|
|
210
|
-
{showMinMax && <Text style={
|
|
275
|
+
{showMinMax && <Text style={tokenStyles.rangeLabel}>{min}</Text>}
|
|
211
276
|
|
|
212
277
|
{/* ── Track container — the gesture zone ──────────────────────────── */}
|
|
213
278
|
<View
|
|
@@ -216,13 +281,13 @@ export function Slider({
|
|
|
216
281
|
{...panResponder.panHandlers}
|
|
217
282
|
>
|
|
218
283
|
{/* Track background (unfilled — full width) */}
|
|
219
|
-
<View style={
|
|
284
|
+
<View style={tokenStyles.trackBg} />
|
|
220
285
|
|
|
221
286
|
{/* Track fill (filled — up to thumb center) */}
|
|
222
|
-
<View style={[
|
|
287
|
+
<View style={[tokenStyles.trackFill, { width: fillWidth }]} />
|
|
223
288
|
|
|
224
289
|
{/* ── Thumb ─────────────────────────────────────────────────────── */}
|
|
225
|
-
<View style={[
|
|
290
|
+
<View style={[tokenStyles.thumb, { left: thumbLeft }, thumbShadow]}>
|
|
226
291
|
{/* Focus ring — 1px outside thumb (26×26 centered over 24×24) */}
|
|
227
292
|
{focused && (
|
|
228
293
|
<View
|
|
@@ -240,48 +305,17 @@ export function Slider({
|
|
|
240
305
|
</View>
|
|
241
306
|
|
|
242
307
|
{/* Max label */}
|
|
243
|
-
{showMinMax && <Text style={
|
|
308
|
+
{showMinMax && <Text style={tokenStyles.rangeLabel}>{max}</Text>}
|
|
244
309
|
</View>
|
|
245
310
|
</View>
|
|
246
311
|
);
|
|
247
312
|
}
|
|
248
313
|
|
|
249
314
|
const styles = StyleSheet.create({
|
|
250
|
-
root: {
|
|
251
|
-
gap: dim.spacing.padding.sysPadding8,
|
|
252
|
-
alignSelf: 'stretch',
|
|
253
|
-
},
|
|
254
|
-
|
|
255
|
-
// ── Label ─────────────────────────────────────────────────────────────────
|
|
256
|
-
label: {
|
|
257
|
-
color: cr.surface.surface.sysOnSurface,
|
|
258
|
-
fontSize: ts.labelMedium.sysFontSize,
|
|
259
|
-
lineHeight: ts.labelMedium.sysLineHeight,
|
|
260
|
-
letterSpacing: ts.labelMedium.sysTracking,
|
|
261
|
-
fontWeight: '400',
|
|
262
|
-
includeFontPadding: false,
|
|
263
|
-
},
|
|
264
|
-
|
|
265
|
-
// ── Row ───────────────────────────────────────────────────────────────────
|
|
266
|
-
row: {
|
|
267
|
-
flexDirection: 'row',
|
|
268
|
-
alignItems: 'center',
|
|
269
|
-
gap: dim.spacing.padding.sysPadding8,
|
|
270
|
-
},
|
|
271
315
|
rowDisabled: {
|
|
272
316
|
opacity: 0.48,
|
|
273
317
|
},
|
|
274
318
|
|
|
275
|
-
// ── Range labels ──────────────────────────────────────────────────────────
|
|
276
|
-
rangeLabel: {
|
|
277
|
-
color: cr.surface.surface.sysOnSurfaceVariant,
|
|
278
|
-
fontSize: ts.bodySmall.sysFontSize,
|
|
279
|
-
lineHeight: ts.bodySmall.sysLineHeight,
|
|
280
|
-
fontWeight: '400',
|
|
281
|
-
includeFontPadding: false,
|
|
282
|
-
flexShrink: 0,
|
|
283
|
-
},
|
|
284
|
-
|
|
285
319
|
// ── Track container ───────────────────────────────────────────────────────
|
|
286
320
|
trackContainer: {
|
|
287
321
|
flex: 1,
|
|
@@ -291,40 +325,6 @@ const styles = StyleSheet.create({
|
|
|
291
325
|
overflow: 'visible',
|
|
292
326
|
},
|
|
293
327
|
|
|
294
|
-
// ── Track background ──────────────────────────────────────────────────────
|
|
295
|
-
trackBg: {
|
|
296
|
-
position: 'absolute',
|
|
297
|
-
top: TRACK_TOP,
|
|
298
|
-
left: 0,
|
|
299
|
-
right: 0,
|
|
300
|
-
height: TRACK_HEIGHT,
|
|
301
|
-
borderRadius: 9999,
|
|
302
|
-
backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerHighest,
|
|
303
|
-
},
|
|
304
|
-
|
|
305
|
-
// ── Track fill ────────────────────────────────────────────────────────────
|
|
306
|
-
trackFill: {
|
|
307
|
-
position: 'absolute',
|
|
308
|
-
top: TRACK_TOP,
|
|
309
|
-
left: 0,
|
|
310
|
-
height: TRACK_HEIGHT,
|
|
311
|
-
borderRadius: 9999,
|
|
312
|
-
backgroundColor: cr.accent.primary.sysPrimary,
|
|
313
|
-
},
|
|
314
|
-
|
|
315
|
-
// ── Thumb ─────────────────────────────────────────────────────────────────
|
|
316
|
-
thumb: {
|
|
317
|
-
position: 'absolute',
|
|
318
|
-
top: THUMB_TOP,
|
|
319
|
-
width: THUMB_SIZE,
|
|
320
|
-
height: THUMB_SIZE,
|
|
321
|
-
borderRadius: 9999,
|
|
322
|
-
backgroundColor: cr.surface.surface.sysSurface,
|
|
323
|
-
// Focus ring is absolutely positioned inside the thumb
|
|
324
|
-
alignItems: 'center',
|
|
325
|
-
justifyContent: 'center',
|
|
326
|
-
},
|
|
327
|
-
|
|
328
328
|
// ── Focus ring: 1px outside the 24×24 thumb → 26×26 ─────────────────────
|
|
329
329
|
focusRing: {
|
|
330
330
|
position: 'absolute',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useRef } from 'react';
|
|
1
|
+
import React, { useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
Animated,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
|
|
12
12
|
import { Ionicons } from '@expo/vector-icons';
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { useTheme } from '../../theme';
|
|
15
15
|
import { BodyText } from '../BodyText';
|
|
16
16
|
|
|
17
17
|
export type SnackbarVariant = 'filled' | 'outlined';
|
|
@@ -26,8 +26,6 @@ export interface SnackbarProps {
|
|
|
26
26
|
style?: StyleProp<ViewStyle>;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const { colorRoles: cr, dimensions: dim } = sys;
|
|
30
|
-
|
|
31
29
|
export function Snackbar({
|
|
32
30
|
visible,
|
|
33
31
|
message,
|
|
@@ -37,6 +35,8 @@ export function Snackbar({
|
|
|
37
35
|
onClose,
|
|
38
36
|
style,
|
|
39
37
|
}: SnackbarProps) {
|
|
38
|
+
const { colorRoles: cr, dimensions: dim } = useTheme();
|
|
39
|
+
|
|
40
40
|
const translateY = useRef(new Animated.Value(100)).current;
|
|
41
41
|
const opacity = useRef(new Animated.Value(0)).current;
|
|
42
42
|
|
|
@@ -73,9 +73,61 @@ export function Snackbar({
|
|
|
73
73
|
|
|
74
74
|
const isFilled = variant === 'filled';
|
|
75
75
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
const tokenStyles = useMemo(
|
|
77
|
+
() => ({
|
|
78
|
+
wrapper: {
|
|
79
|
+
position: 'absolute' as const,
|
|
80
|
+
bottom: dim.spacing.padding.sysPadding24,
|
|
81
|
+
left: dim.spacing.padding.sysPadding16,
|
|
82
|
+
right: dim.spacing.padding.sysPadding16,
|
|
83
|
+
alignItems: 'center' as const,
|
|
84
|
+
},
|
|
85
|
+
container: {
|
|
86
|
+
flexDirection: 'row' as const,
|
|
87
|
+
alignItems: 'center' as const,
|
|
88
|
+
paddingVertical: dim.spacing.padding.sysPadding12,
|
|
89
|
+
paddingHorizontal: dim.spacing.padding.sysPadding16,
|
|
90
|
+
borderRadius: dim.borderRadius.sysRadiusMd,
|
|
91
|
+
gap: dim.spacing.padding.sysPadding16,
|
|
92
|
+
width: '100%' as const,
|
|
93
|
+
// Elevation/lv3: Figma radius=16 (primary), radius=6 (secondary). RN iOS
|
|
94
|
+
// supports one shadow; use the dominant layer. Android uses elevation.
|
|
95
|
+
shadowColor: '#000',
|
|
96
|
+
shadowOffset: { width: 0, height: 4 },
|
|
97
|
+
shadowOpacity: 0.16,
|
|
98
|
+
shadowRadius: 16,
|
|
99
|
+
elevation: 6,
|
|
100
|
+
},
|
|
101
|
+
containerFilled: {
|
|
102
|
+
backgroundColor: cr.accent.primary.sysPrimaryContainer,
|
|
103
|
+
},
|
|
104
|
+
containerOutlined: {
|
|
105
|
+
backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
|
|
106
|
+
borderWidth: dim.borderWidth.sysStrokeThin,
|
|
107
|
+
borderColor: cr.outline.sysOutlineVariant,
|
|
108
|
+
},
|
|
109
|
+
actionSlot: {
|
|
110
|
+
flexDirection: 'row' as const,
|
|
111
|
+
alignItems: 'center' as const,
|
|
112
|
+
gap: dim.spacing.padding.sysPadding12,
|
|
113
|
+
},
|
|
114
|
+
actionButton: {
|
|
115
|
+
paddingVertical: dim.spacing.padding.sysPadding4,
|
|
116
|
+
},
|
|
117
|
+
closeButton: {
|
|
118
|
+
width: 24,
|
|
119
|
+
height: 24,
|
|
120
|
+
borderRadius: dim.borderRadius.sysRadiusFull,
|
|
121
|
+
alignItems: 'center' as const,
|
|
122
|
+
justifyContent: 'center' as const,
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
[cr, dim],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const containerVariantStyle = isFilled
|
|
129
|
+
? tokenStyles.containerFilled
|
|
130
|
+
: tokenStyles.containerOutlined;
|
|
79
131
|
const labelColor = isFilled
|
|
80
132
|
? cr.accent.primary.sysOnPrimary
|
|
81
133
|
: cr.surface.surface.sysOnSurface;
|
|
@@ -91,10 +143,14 @@ export function Snackbar({
|
|
|
91
143
|
|
|
92
144
|
return (
|
|
93
145
|
<Animated.View
|
|
94
|
-
style={[
|
|
146
|
+
style={[
|
|
147
|
+
tokenStyles.wrapper,
|
|
148
|
+
{ opacity, transform: [{ translateY }] },
|
|
149
|
+
style,
|
|
150
|
+
]}
|
|
95
151
|
pointerEvents={visible ? 'box-none' : 'none'}
|
|
96
152
|
>
|
|
97
|
-
<View style={[
|
|
153
|
+
<View style={[tokenStyles.container, containerVariantStyle]}>
|
|
98
154
|
<BodyText
|
|
99
155
|
variant="medium"
|
|
100
156
|
color={labelColor}
|
|
@@ -104,18 +160,18 @@ export function Snackbar({
|
|
|
104
160
|
{message}
|
|
105
161
|
</BodyText>
|
|
106
162
|
|
|
107
|
-
<View style={
|
|
163
|
+
<View style={tokenStyles.actionSlot}>
|
|
108
164
|
{actionLabel && onAction && (
|
|
109
165
|
<Pressable
|
|
110
166
|
onPress={onAction}
|
|
111
167
|
accessibilityRole="button"
|
|
112
168
|
accessibilityLabel={actionLabel}
|
|
113
169
|
style={({ pressed }) => [
|
|
114
|
-
|
|
170
|
+
tokenStyles.actionButton,
|
|
115
171
|
pressed && styles.pressed,
|
|
116
172
|
]}
|
|
117
173
|
>
|
|
118
|
-
<BodyText variant="
|
|
174
|
+
<BodyText variant="labelMedium" color={actionColor}>
|
|
119
175
|
{actionLabel}
|
|
120
176
|
</BodyText>
|
|
121
177
|
</Pressable>
|
|
@@ -127,7 +183,7 @@ export function Snackbar({
|
|
|
127
183
|
accessibilityRole="button"
|
|
128
184
|
accessibilityLabel="Dismiss"
|
|
129
185
|
style={({ pressed }) => [
|
|
130
|
-
|
|
186
|
+
tokenStyles.closeButton,
|
|
131
187
|
{ backgroundColor: closeBg },
|
|
132
188
|
pressed && styles.pressed,
|
|
133
189
|
]}
|
|
@@ -142,53 +198,9 @@ export function Snackbar({
|
|
|
142
198
|
}
|
|
143
199
|
|
|
144
200
|
const styles = StyleSheet.create({
|
|
145
|
-
wrapper: {
|
|
146
|
-
position: 'absolute',
|
|
147
|
-
bottom: dim.spacing.padding.sysPadding24,
|
|
148
|
-
left: dim.spacing.padding.sysPadding16,
|
|
149
|
-
right: dim.spacing.padding.sysPadding16,
|
|
150
|
-
alignItems: 'center',
|
|
151
|
-
},
|
|
152
|
-
container: {
|
|
153
|
-
flexDirection: 'row',
|
|
154
|
-
alignItems: 'center',
|
|
155
|
-
paddingVertical: dim.spacing.padding.sysPadding12,
|
|
156
|
-
paddingHorizontal: dim.spacing.padding.sysPadding16,
|
|
157
|
-
borderRadius: dim.borderRadius.sysRadiusMd,
|
|
158
|
-
gap: dim.spacing.padding.sysPadding16,
|
|
159
|
-
width: '100%',
|
|
160
|
-
shadowColor: '#000',
|
|
161
|
-
shadowOffset: { width: 0, height: 4 },
|
|
162
|
-
shadowOpacity: 0.12,
|
|
163
|
-
shadowRadius: 12,
|
|
164
|
-
elevation: 6,
|
|
165
|
-
},
|
|
166
|
-
containerFilled: {
|
|
167
|
-
backgroundColor: cr.accent.primary.sysPrimaryContainer,
|
|
168
|
-
},
|
|
169
|
-
containerOutlined: {
|
|
170
|
-
backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
|
|
171
|
-
borderWidth: dim.borderWidth.sysStrokeThin,
|
|
172
|
-
borderColor: cr.outline.sysOutline,
|
|
173
|
-
},
|
|
174
201
|
label: {
|
|
175
202
|
flex: 1,
|
|
176
203
|
},
|
|
177
|
-
actionSlot: {
|
|
178
|
-
flexDirection: 'row',
|
|
179
|
-
alignItems: 'center',
|
|
180
|
-
gap: dim.spacing.padding.sysPadding12,
|
|
181
|
-
},
|
|
182
|
-
actionButton: {
|
|
183
|
-
paddingVertical: dim.spacing.padding.sysPadding4,
|
|
184
|
-
},
|
|
185
|
-
closeButton: {
|
|
186
|
-
width: 24,
|
|
187
|
-
height: 24,
|
|
188
|
-
borderRadius: dim.borderRadius.sysRadiusFull,
|
|
189
|
-
alignItems: 'center',
|
|
190
|
-
justifyContent: 'center',
|
|
191
|
-
},
|
|
192
204
|
pressed: {
|
|
193
205
|
opacity: 0.7,
|
|
194
206
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
Animated,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
ViewStyle,
|
|
10
10
|
} from 'react-native';
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { useTheme } from '../../theme';
|
|
13
13
|
|
|
14
14
|
export interface SwitchProps {
|
|
15
15
|
/** Controlled value. Omit to use internal state. */
|
|
@@ -23,17 +23,10 @@ export interface SwitchProps {
|
|
|
23
23
|
style?: StyleProp<ViewStyle>;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const { colorRoles: cr, dimensions: dim } = sys;
|
|
27
|
-
|
|
28
26
|
// ── Layout constants ──────────────────────────────────────────────────────────
|
|
29
27
|
const TRACK_W = 56;
|
|
30
28
|
const TRACK_H = 32;
|
|
31
29
|
const THUMB_SIZE = 24;
|
|
32
|
-
const TRACK_PADDING = dim.spacing.padding.sysPadding4;
|
|
33
|
-
|
|
34
|
-
// Thumb translateX: off=left edge, on=right edge
|
|
35
|
-
const THUMB_OFF = TRACK_PADDING; // 4
|
|
36
|
-
const THUMB_ON = TRACK_W - TRACK_PADDING - THUMB_SIZE; // 56 - 4 - 24 = 28
|
|
37
30
|
|
|
38
31
|
// ── Component ─────────────────────────────────────────────────────────────────
|
|
39
32
|
|
|
@@ -45,6 +38,14 @@ export function Switch({
|
|
|
45
38
|
accessibilityLabel,
|
|
46
39
|
style,
|
|
47
40
|
}: SwitchProps) {
|
|
41
|
+
const { colorRoles: cr, dimensions: dim } = useTheme();
|
|
42
|
+
|
|
43
|
+
const trackPadding = useMemo(() => dim.spacing.padding.sysPadding4, [dim]);
|
|
44
|
+
|
|
45
|
+
// Thumb translateX: off=left edge, on=right edge
|
|
46
|
+
const thumbOff = trackPadding; // 4
|
|
47
|
+
const thumbOn = TRACK_W - trackPadding - THUMB_SIZE; // 56 - 4 - 24 = 28
|
|
48
|
+
|
|
48
49
|
// Controlled vs uncontrolled
|
|
49
50
|
const isControlled = valueProp !== undefined;
|
|
50
51
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
@@ -54,14 +55,14 @@ export function Switch({
|
|
|
54
55
|
|
|
55
56
|
// ── Animation ──────────────────────────────────────────────────────────────
|
|
56
57
|
const thumbAnim = useRef(
|
|
57
|
-
new Animated.Value(toggled ?
|
|
58
|
+
new Animated.Value(toggled ? thumbOn : thumbOff),
|
|
58
59
|
).current;
|
|
59
60
|
const bgAnim = useRef(new Animated.Value(toggled ? 1 : 0)).current;
|
|
60
61
|
|
|
61
62
|
useEffect(() => {
|
|
62
63
|
Animated.parallel([
|
|
63
64
|
Animated.timing(thumbAnim, {
|
|
64
|
-
toValue: toggled ?
|
|
65
|
+
toValue: toggled ? thumbOn : thumbOff,
|
|
65
66
|
duration: 150,
|
|
66
67
|
useNativeDriver: true,
|
|
67
68
|
}),
|
|
@@ -137,6 +138,7 @@ export function Switch({
|
|
|
137
138
|
style={[
|
|
138
139
|
styles.thumb,
|
|
139
140
|
{
|
|
141
|
+
top: trackPadding,
|
|
140
142
|
backgroundColor: thumbBg,
|
|
141
143
|
transform: [{ translateX: thumbAnim }],
|
|
142
144
|
},
|
|
@@ -167,7 +169,6 @@ const styles = StyleSheet.create({
|
|
|
167
169
|
},
|
|
168
170
|
thumb: {
|
|
169
171
|
position: 'absolute',
|
|
170
|
-
top: TRACK_PADDING,
|
|
171
172
|
left: 0, // translateX drives actual X position
|
|
172
173
|
width: THUMB_SIZE,
|
|
173
174
|
height: THUMB_SIZE,
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { useTheme } from '../../theme';
|
|
6
6
|
|
|
7
7
|
export type TooltipVariant = 'filled' | 'elevated';
|
|
8
8
|
/**
|
|
@@ -24,8 +24,6 @@ export interface TooltipProps {
|
|
|
24
24
|
style?: StyleProp<ViewStyle>;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
|
|
28
|
-
|
|
29
27
|
// ── Arrow dimensions ─────────────────────────────────────────────────────────
|
|
30
28
|
const ARROW_BASE = 16; // px — wide axis
|
|
31
29
|
const ARROW_HEIGHT = 6; // px — narrow axis
|
|
@@ -38,6 +36,8 @@ export function Tooltip({
|
|
|
38
36
|
direction = 'none',
|
|
39
37
|
style,
|
|
40
38
|
}: TooltipProps) {
|
|
39
|
+
const { colorRoles: cr, dimensions: dim, typeScale: ts } = useTheme();
|
|
40
|
+
|
|
41
41
|
const isFilled = variant === 'filled';
|
|
42
42
|
|
|
43
43
|
const bg = isFilled
|
package/src/index.ts
CHANGED
|
@@ -131,6 +131,15 @@ export type {
|
|
|
131
131
|
ActionSheetAction,
|
|
132
132
|
} from './components/ActionSheet';
|
|
133
133
|
|
|
134
|
+
export { ChatBubble } from './components/ChatBubble';
|
|
135
|
+
export type {
|
|
136
|
+
ChatBubbleProps,
|
|
137
|
+
ChatBubbleVariant,
|
|
138
|
+
} from './components/ChatBubble';
|
|
139
|
+
|
|
140
|
+
export { ChatInput } from './components/ChatInput';
|
|
141
|
+
export type { ChatInputProps, ChatAttachment } from './components/ChatInput';
|
|
142
|
+
|
|
134
143
|
export { BodyText } from './components/BodyText';
|
|
135
144
|
export type { BodyTextProps, BodyVariant } from './components/BodyText';
|
|
136
145
|
|
|
@@ -140,6 +149,10 @@ export type { HeaderTextProps, HeaderVariant } from './components/HeaderText';
|
|
|
140
149
|
// Re-export tokens for consumers who need direct access
|
|
141
150
|
export { sys } from './tokens';
|
|
142
151
|
|
|
152
|
+
// Theme system — override the default GuidanceNow theme per-app or per-subtree
|
|
153
|
+
export { ThemeProvider, useTheme, defaultTheme } from './theme';
|
|
154
|
+
export type { ThemeProviderProps, Theme, ThemeOverride } from './theme';
|
|
155
|
+
|
|
143
156
|
// Icons
|
|
144
157
|
export {
|
|
145
158
|
UserRoundIcon,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
|
|
3
|
+
import { sys } from '../tokens';
|
|
4
|
+
import type { Theme } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Provides the active resolved theme to all components.
|
|
8
|
+
* Default value is the GuidanceNow theme so components render correctly
|
|
9
|
+
* even when no ThemeProvider is present in the tree.
|
|
10
|
+
*/
|
|
11
|
+
export const ThemeContext = createContext<Theme>(sys as unknown as Theme);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { ThemeContext } from './ThemeContext';
|
|
4
|
+
import { mergeTheme } from './mergeTheme';
|
|
5
|
+
import type { ThemeOverride } from './types';
|
|
6
|
+
|
|
7
|
+
export interface ThemeProviderProps {
|
|
8
|
+
/**
|
|
9
|
+
* Optional partial theme override. Keys present here replace the
|
|
10
|
+
* corresponding GuidanceNow default values; all other tokens keep their
|
|
11
|
+
* defaults. Pass `undefined` (or omit this prop) to use the GuidanceNow
|
|
12
|
+
* theme unchanged.
|
|
13
|
+
*/
|
|
14
|
+
theme?: ThemeOverride;
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Wraps your application (or a subtree) and provides a resolved theme to all
|
|
20
|
+
* `@compsych/mobile-ui` components beneath it.
|
|
21
|
+
*
|
|
22
|
+
* @example — default GuidanceNow theme
|
|
23
|
+
* <ThemeProvider>
|
|
24
|
+
* <App />
|
|
25
|
+
* </ThemeProvider>
|
|
26
|
+
*
|
|
27
|
+
* @example — custom brand override
|
|
28
|
+
* <ThemeProvider theme={{ colorRoles: { accent: { primary: { sysPrimary: '#c00000' } } } }}>
|
|
29
|
+
* <App />
|
|
30
|
+
* </ThemeProvider>
|
|
31
|
+
*/
|
|
32
|
+
export function ThemeProvider({ theme, children }: ThemeProviderProps) {
|
|
33
|
+
const resolvedTheme = useMemo(() => mergeTheme(theme), [theme]);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<ThemeContext.Provider value={resolvedTheme}>
|
|
37
|
+
{children}
|
|
38
|
+
</ThemeContext.Provider>
|
|
39
|
+
);
|
|
40
|
+
}
|