@retray-dev/ui-kit 9.3.1 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMPONENTS.md +136 -22
- package/CONSUMER.md +48 -8
- package/FONTS.md +54 -13
- package/README.md +40 -3
- package/dist/Accordion.d.mts +1 -1
- package/dist/Accordion.d.ts +1 -1
- package/dist/Accordion.js +2 -2
- package/dist/Accordion.mjs +1 -1
- package/dist/ConfirmDialog.d.mts +6 -1
- package/dist/ConfirmDialog.d.ts +6 -1
- package/dist/ConfirmDialog.js +44 -14
- package/dist/ConfirmDialog.mjs +1 -1
- package/dist/ImageViewer.js +282 -141
- package/dist/ImageViewer.mjs +3 -1
- package/dist/Sheet.js +16 -13
- package/dist/Sheet.mjs +1 -1
- package/dist/Switch.js +40 -17
- package/dist/Switch.mjs +1 -1
- package/dist/{chunk-O3HA6TYM.mjs → chunk-DJ7RN37L.mjs} +2 -2
- package/dist/{chunk-FZZLPJ6B.mjs → chunk-KZL5VTYK.mjs} +43 -14
- package/dist/{chunk-QKH5ZOD5.mjs → chunk-WF2XDFRK.mjs} +40 -17
- package/dist/{chunk-Z4BVUWW6.mjs → chunk-WOEYDUJZ.mjs} +19 -31
- package/dist/{chunk-PFZTM6D5.mjs → chunk-Y2NS74WS.mjs} +9 -7
- package/dist/fonts.d.mts +39 -31
- package/dist/fonts.d.ts +39 -31
- package/dist/fonts.js +34 -39
- package/dist/fonts.mjs +35 -34
- package/dist/index.js +119 -76
- package/dist/index.mjs +5 -5
- package/package.json +3 -1
- package/scripts/build-apk.sh +84 -0
- package/scripts/copy-fonts.js +90 -0
- package/scripts/test-consumer-fonts.sh +82 -0
- package/src/components/Accordion/Accordion.tsx +7 -3
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +61 -23
- package/src/components/ImageViewer/ImageViewer.tsx +25 -30
- package/src/components/Sheet/Sheet.tsx +10 -9
- package/src/components/Switch/Switch.tsx +30 -17
- package/src/fonts.ts +59 -40
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Verifies that dist/fonts.js paths resolve correctly from a consumer's perspective.
|
|
3
|
+
# Simulates what npm publish sends: packs the package, installs into a temp Expo app,
|
|
4
|
+
# then checks that Metro can find all .otf files without ../traversal.
|
|
5
|
+
#
|
|
6
|
+
# Run from repo root: ./scripts/test-consumer-fonts.sh
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
10
|
+
TMP="$(mktemp -d)"
|
|
11
|
+
PACK_DIR="$TMP/pack"
|
|
12
|
+
|
|
13
|
+
# Find npm — prefer nvm node, fallback to PATH
|
|
14
|
+
NPM_BIN=""
|
|
15
|
+
for node_dir in "$HOME"/.nvm/versions/node/*/bin; do
|
|
16
|
+
if [ -f "$node_dir/npm" ]; then NPM_BIN="$node_dir/npm"; fi
|
|
17
|
+
done
|
|
18
|
+
if [ -z "$NPM_BIN" ]; then NPM_BIN="npm"; fi
|
|
19
|
+
|
|
20
|
+
cleanup() { rm -rf "$TMP"; }
|
|
21
|
+
trap cleanup EXIT
|
|
22
|
+
|
|
23
|
+
echo "=== 1. Build ui-kit ==="
|
|
24
|
+
cd "$ROOT"
|
|
25
|
+
pnpm build
|
|
26
|
+
|
|
27
|
+
echo ""
|
|
28
|
+
echo "=== 2. Pack (simulates npm publish tarball) ==="
|
|
29
|
+
mkdir -p "$PACK_DIR"
|
|
30
|
+
cd "$ROOT"
|
|
31
|
+
TARBALL=$("$NPM_BIN" pack --pack-destination "$PACK_DIR" 2>/dev/null | tail -1)
|
|
32
|
+
TARBALL_PATH="$PACK_DIR/$TARBALL"
|
|
33
|
+
echo "Packed: $TARBALL_PATH"
|
|
34
|
+
|
|
35
|
+
echo ""
|
|
36
|
+
echo "=== 3. Verify tarball contents ==="
|
|
37
|
+
echo "Files in tarball matching fonts:"
|
|
38
|
+
tar -tzf "$TARBALL_PATH" | grep -E "(fonts|\.otf)" | sort
|
|
39
|
+
|
|
40
|
+
echo ""
|
|
41
|
+
echo "=== 4. Check dist/fonts.js paths stay within dist/ ==="
|
|
42
|
+
tar -xzf "$TARBALL_PATH" -C "$TMP" package/dist/fonts.js 2>/dev/null
|
|
43
|
+
|
|
44
|
+
FONTS_JS="$TMP/package/dist/fonts.js"
|
|
45
|
+
if grep -q "\.\./src" "$FONTS_JS"; then
|
|
46
|
+
echo "FAIL: dist/fonts.js still contains ../src traversal:"
|
|
47
|
+
grep "\.\./src" "$FONTS_JS"
|
|
48
|
+
exit 1
|
|
49
|
+
else
|
|
50
|
+
echo "OK: No ../src traversal found in dist/fonts.js"
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
echo ""
|
|
54
|
+
echo "=== 5. Verify all 28 .otf files exist in dist/assets/fonts/ ==="
|
|
55
|
+
OTF_COUNT=$(tar -tzf "$TARBALL_PATH" | grep "^package/dist/assets/fonts/.*\.otf$" | wc -l | tr -d ' ')
|
|
56
|
+
if [ "$OTF_COUNT" -eq 28 ]; then
|
|
57
|
+
echo "OK: 28 .otf files present in dist/assets/fonts/ inside tarball"
|
|
58
|
+
else
|
|
59
|
+
echo "FAIL: expected 28 .otf files in dist/assets/fonts/, found $OTF_COUNT"
|
|
60
|
+
tar -tzf "$TARBALL_PATH" | grep "^package/dist/assets/fonts/.*\.otf$"
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
echo ""
|
|
65
|
+
echo "=== 6. Validate require paths — no ../traversal ==="
|
|
66
|
+
TRAVERSALS=$(grep -oE '"[^"]+\.otf"' "$FONTS_JS" | grep "\.\." || true)
|
|
67
|
+
if [ -n "$TRAVERSALS" ]; then
|
|
68
|
+
echo "FAIL: Traversal paths found in dist/fonts.js:"
|
|
69
|
+
echo "$TRAVERSALS"
|
|
70
|
+
exit 1
|
|
71
|
+
else
|
|
72
|
+
echo "OK: All .otf require() paths use ./ (within dist/)"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
echo ""
|
|
76
|
+
echo "=== PASS: Consumer font resolution looks correct ==="
|
|
77
|
+
echo "dist/fonts.js uses ./assets/fonts/ paths"
|
|
78
|
+
echo "All 28 .otf files are present in dist/assets/fonts/ in the tarball"
|
|
79
|
+
echo ""
|
|
80
|
+
echo "Consumer usage (no watchFolders hack needed):"
|
|
81
|
+
echo " import { SohneFonts } from '@retray-dev/ui-kit/fonts'"
|
|
82
|
+
echo " useFonts(SohneFonts)"
|
|
@@ -21,7 +21,7 @@ import { TIMINGS, EASINGS } from '../../utils/animations'
|
|
|
21
21
|
|
|
22
22
|
export interface AccordionItem {
|
|
23
23
|
value: string
|
|
24
|
-
trigger: string
|
|
24
|
+
trigger: string | React.ReactNode
|
|
25
25
|
content: React.ReactNode
|
|
26
26
|
/** Icon name from @expo/vector-icons rendered left of trigger. */
|
|
27
27
|
iconName?: string
|
|
@@ -102,11 +102,15 @@ function AccordionItemComponent({
|
|
|
102
102
|
}}
|
|
103
103
|
accessibilityRole="button"
|
|
104
104
|
accessibilityState={{ expanded: isOpen }}
|
|
105
|
-
accessibilityLabel={item.trigger}
|
|
105
|
+
accessibilityLabel={typeof item.trigger === 'string' ? item.trigger : undefined}
|
|
106
106
|
>
|
|
107
107
|
<View style={styles.triggerContent}>
|
|
108
108
|
{resolvedIcon ? <View style={styles.icon}>{resolvedIcon}</View> : null}
|
|
109
|
-
|
|
109
|
+
{typeof item.trigger === 'string' ? (
|
|
110
|
+
<Text style={[styles.triggerText, { color: colors.foreground }]} allowFontScaling={true}>{item.trigger}</Text>
|
|
111
|
+
) : (
|
|
112
|
+
item.trigger
|
|
113
|
+
)}
|
|
110
114
|
</View>
|
|
111
115
|
<Animated.View style={[styles.chevron, rotationStyle]}>
|
|
112
116
|
<Entypo name="chevron-down" size={18} color={colors.foregroundMuted} />
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react'
|
|
2
|
-
import { View, Text, StyleSheet } from 'react-native'
|
|
3
|
-
import {
|
|
4
|
-
BottomSheetModal,
|
|
2
|
+
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'
|
|
3
|
+
import BottomSheet, {
|
|
5
4
|
BottomSheetView,
|
|
6
5
|
BottomSheetBackdrop,
|
|
7
6
|
type BottomSheetBackdropProps,
|
|
@@ -15,12 +14,17 @@ import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
|
15
14
|
export interface ConfirmDialogProps {
|
|
16
15
|
visible: boolean
|
|
17
16
|
title: string
|
|
17
|
+
/** Secondary text below title. */
|
|
18
|
+
subtitle?: string
|
|
19
|
+
/** @deprecated Use `subtitle` instead. */
|
|
18
20
|
description?: string
|
|
19
21
|
confirmLabel?: string
|
|
20
22
|
cancelLabel?: string
|
|
21
23
|
confirmVariant?: 'primary' | 'destructive'
|
|
22
24
|
/** Show a loading spinner in the confirm button (e.g. while async action completes). */
|
|
23
25
|
loading?: boolean
|
|
26
|
+
/** Show an X close button in the top-right corner. */
|
|
27
|
+
showCloseButton?: boolean
|
|
24
28
|
onConfirm: () => void
|
|
25
29
|
onCancel: () => void
|
|
26
30
|
}
|
|
@@ -28,23 +32,26 @@ export interface ConfirmDialogProps {
|
|
|
28
32
|
export function ConfirmDialog({
|
|
29
33
|
visible,
|
|
30
34
|
title,
|
|
35
|
+
subtitle,
|
|
31
36
|
description,
|
|
32
37
|
confirmLabel = 'Confirm',
|
|
33
38
|
cancelLabel = 'Cancel',
|
|
34
39
|
confirmVariant = 'primary',
|
|
35
40
|
loading = false,
|
|
41
|
+
showCloseButton = false,
|
|
36
42
|
onConfirm,
|
|
37
43
|
onCancel,
|
|
38
44
|
}: ConfirmDialogProps) {
|
|
39
45
|
const { colors } = useTheme()
|
|
40
|
-
const ref = useRef<
|
|
46
|
+
const ref = useRef<BottomSheet>(null)
|
|
47
|
+
const effectiveSubtitle = subtitle ?? description
|
|
41
48
|
|
|
42
49
|
useEffect(() => {
|
|
43
50
|
if (visible) {
|
|
44
51
|
impactMedium()
|
|
45
|
-
ref.current?.
|
|
52
|
+
ref.current?.snapToIndex(0)
|
|
46
53
|
} else {
|
|
47
|
-
ref.current?.
|
|
54
|
+
ref.current?.close()
|
|
48
55
|
}
|
|
49
56
|
}, [visible])
|
|
50
57
|
|
|
@@ -58,24 +65,42 @@ export function ConfirmDialog({
|
|
|
58
65
|
)
|
|
59
66
|
|
|
60
67
|
return (
|
|
61
|
-
<
|
|
68
|
+
<BottomSheet
|
|
62
69
|
ref={ref}
|
|
70
|
+
index={-1}
|
|
71
|
+
onClose={onCancel}
|
|
63
72
|
enableDynamicSizing
|
|
64
|
-
onDismiss={onCancel}
|
|
65
73
|
backdropComponent={renderBackdrop}
|
|
66
74
|
backgroundStyle={[styles.background, { backgroundColor: colors.card }]}
|
|
67
75
|
handleIndicatorStyle={[styles.handle, { backgroundColor: colors.border }]}
|
|
68
76
|
enablePanDownToClose
|
|
69
77
|
>
|
|
70
78
|
<BottomSheetView style={styles.content}>
|
|
71
|
-
<
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
<View style={styles.header} accessibilityRole="header">
|
|
80
|
+
<View style={styles.headerRow}>
|
|
81
|
+
<Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>
|
|
82
|
+
{title}
|
|
83
|
+
</Text>
|
|
84
|
+
{showCloseButton ? (
|
|
85
|
+
<TouchableOpacity
|
|
86
|
+
onPress={onCancel}
|
|
87
|
+
style={styles.closeButton}
|
|
88
|
+
activeOpacity={0.6}
|
|
89
|
+
touchSoundDisabled={true}
|
|
90
|
+
accessibilityRole="button"
|
|
91
|
+
accessibilityLabel="Close"
|
|
92
|
+
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
|
|
93
|
+
>
|
|
94
|
+
<Feather name="x" size={ms(18)} color={colors.foregroundMuted} />
|
|
95
|
+
</TouchableOpacity>
|
|
96
|
+
) : null}
|
|
97
|
+
</View>
|
|
98
|
+
{effectiveSubtitle ? (
|
|
99
|
+
<Text style={[styles.subtitle, { color: colors.foregroundMuted }]} allowFontScaling={true}>
|
|
100
|
+
{effectiveSubtitle}
|
|
101
|
+
</Text>
|
|
102
|
+
) : null}
|
|
103
|
+
</View>
|
|
79
104
|
<View style={styles.actions}>
|
|
80
105
|
<Button
|
|
81
106
|
label={confirmLabel}
|
|
@@ -105,7 +130,7 @@ export function ConfirmDialog({
|
|
|
105
130
|
/>
|
|
106
131
|
</View>
|
|
107
132
|
</BottomSheetView>
|
|
108
|
-
</
|
|
133
|
+
</BottomSheet>
|
|
109
134
|
)
|
|
110
135
|
}
|
|
111
136
|
|
|
@@ -120,19 +145,32 @@ const styles = StyleSheet.create({
|
|
|
120
145
|
borderRadius: ms(2),
|
|
121
146
|
},
|
|
122
147
|
content: {
|
|
123
|
-
paddingHorizontal: s(
|
|
148
|
+
paddingHorizontal: s(16),
|
|
124
149
|
paddingBottom: vs(32),
|
|
125
|
-
|
|
150
|
+
},
|
|
151
|
+
header: {
|
|
152
|
+
paddingTop: vs(4),
|
|
153
|
+
paddingBottom: vs(12),
|
|
154
|
+
gap: vs(4),
|
|
155
|
+
},
|
|
156
|
+
headerRow: {
|
|
157
|
+
flexDirection: 'row',
|
|
158
|
+
alignItems: 'center',
|
|
159
|
+
justifyContent: 'space-between',
|
|
126
160
|
},
|
|
127
161
|
title: {
|
|
128
162
|
fontFamily: 'Sohne-SemiBold',
|
|
129
163
|
fontSize: ms(18),
|
|
130
|
-
|
|
164
|
+
flex: 1,
|
|
165
|
+
},
|
|
166
|
+
closeButton: {
|
|
167
|
+
padding: s(4),
|
|
168
|
+
marginLeft: s(8),
|
|
131
169
|
},
|
|
132
|
-
|
|
170
|
+
subtitle: {
|
|
133
171
|
fontFamily: 'Sohne-Regular',
|
|
134
|
-
fontSize: ms(
|
|
135
|
-
lineHeight: mvs(
|
|
172
|
+
fontSize: ms(14),
|
|
173
|
+
lineHeight: mvs(20),
|
|
136
174
|
},
|
|
137
175
|
actions: {
|
|
138
176
|
gap: vs(10),
|
|
@@ -2,7 +2,8 @@ import React, { useState, useCallback } from 'react'
|
|
|
2
2
|
import {
|
|
3
3
|
Modal,
|
|
4
4
|
View,
|
|
5
|
-
|
|
5
|
+
Image,
|
|
6
|
+
Dimensions,
|
|
6
7
|
StyleSheet,
|
|
7
8
|
useWindowDimensions,
|
|
8
9
|
ImageSourcePropType,
|
|
@@ -18,7 +19,7 @@ import Animated, {
|
|
|
18
19
|
runOnJS,
|
|
19
20
|
} from 'react-native-reanimated'
|
|
20
21
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
21
|
-
import {
|
|
22
|
+
import { IconButton } from '../IconButton'
|
|
22
23
|
import { PagerDots } from '../PagerDots'
|
|
23
24
|
import { s, vs } from '../../utils/scaling'
|
|
24
25
|
|
|
@@ -102,13 +103,11 @@ function ZoomableImage({ source, width, height, onZoomChange }: ZoomableImagePro
|
|
|
102
103
|
|
|
103
104
|
return (
|
|
104
105
|
<GestureDetector gesture={composed}>
|
|
105
|
-
<
|
|
106
|
-
<Animated.
|
|
107
|
-
source={source}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
/>
|
|
111
|
-
</Animated.View>
|
|
106
|
+
<View style={[{ width, height }, styles.imageWrap]} collapsable={false}>
|
|
107
|
+
<Animated.View style={[{ width, height }, animatedStyle]}>
|
|
108
|
+
<Image source={source} style={{ width, height }} resizeMode="contain" />
|
|
109
|
+
</Animated.View>
|
|
110
|
+
</View>
|
|
112
111
|
</GestureDetector>
|
|
113
112
|
)
|
|
114
113
|
}
|
|
@@ -132,7 +131,9 @@ export interface ImageViewerProps {
|
|
|
132
131
|
* <ImageViewer images={pages} visible={open} initialIndex={page} onClose={() => setOpen(false)} />
|
|
133
132
|
*/
|
|
134
133
|
export function ImageViewer({ images, visible, onClose, initialIndex = 0 }: ImageViewerProps) {
|
|
135
|
-
const
|
|
134
|
+
const window = useWindowDimensions()
|
|
135
|
+
const width = window.width > 0 ? window.width : Dimensions.get('window').width
|
|
136
|
+
const height = window.height > 0 ? window.height : Dimensions.get('window').height
|
|
136
137
|
const insets = useSafeAreaInsets()
|
|
137
138
|
const [index, setIndex] = useState(initialIndex)
|
|
138
139
|
const [pagingEnabled, setPagingEnabled] = useState(true)
|
|
@@ -203,7 +204,7 @@ export function ImageViewer({ images, visible, onClose, initialIndex = 0 }: Imag
|
|
|
203
204
|
<Animated.View style={[styles.backdrop, backdropStyle]} pointerEvents="none" />
|
|
204
205
|
<Animated.View style={[styles.container, dismissStyle]}>
|
|
205
206
|
<GestureDetector gesture={swipeDown}>
|
|
206
|
-
<
|
|
207
|
+
<View style={styles.root} collapsable={false}>
|
|
207
208
|
<ScrollView
|
|
208
209
|
ref={scrollRef}
|
|
209
210
|
horizontal
|
|
@@ -223,20 +224,20 @@ export function ImageViewer({ images, visible, onClose, initialIndex = 0 }: Imag
|
|
|
223
224
|
/>
|
|
224
225
|
))}
|
|
225
226
|
</ScrollView>
|
|
226
|
-
</
|
|
227
|
+
</View>
|
|
227
228
|
</GestureDetector>
|
|
228
229
|
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
</
|
|
230
|
+
<View style={[styles.closeButtonWrapper, { top: insets.top + vs(8) }]}>
|
|
231
|
+
<IconButton
|
|
232
|
+
iconName="x"
|
|
233
|
+
size="md"
|
|
234
|
+
variant="text"
|
|
235
|
+
style={{ backgroundColor: 'rgba(255,255,255,0.18)' }}
|
|
236
|
+
iconColor="#fff"
|
|
237
|
+
onPress={onClose}
|
|
238
|
+
accessibilityLabel="Close"
|
|
239
|
+
/>
|
|
240
|
+
</View>
|
|
240
241
|
|
|
241
242
|
{images.length > 1 ? (
|
|
242
243
|
<View style={[styles.dots, { bottom: insets.bottom + vs(16) }]} pointerEvents="box-none">
|
|
@@ -271,15 +272,9 @@ const styles = StyleSheet.create({
|
|
|
271
272
|
justifyContent: 'center',
|
|
272
273
|
overflow: 'hidden',
|
|
273
274
|
},
|
|
274
|
-
|
|
275
|
+
closeButtonWrapper: {
|
|
275
276
|
position: 'absolute',
|
|
276
277
|
right: s(12),
|
|
277
|
-
width: s(40),
|
|
278
|
-
height: s(40),
|
|
279
|
-
borderRadius: s(20),
|
|
280
|
-
backgroundColor: 'rgba(0,0,0,0.4)',
|
|
281
|
-
alignItems: 'center',
|
|
282
|
-
justifyContent: 'center',
|
|
283
278
|
},
|
|
284
279
|
dots: {
|
|
285
280
|
position: 'absolute',
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useRef } from 'react'
|
|
2
2
|
import { View, Text, TouchableOpacity, StyleSheet, ViewStyle, Dimensions, Platform, Modal, ScrollView, useWindowDimensions, Pressable } from 'react-native'
|
|
3
|
-
import {
|
|
4
|
-
BottomSheetModal,
|
|
3
|
+
import BottomSheet, {
|
|
5
4
|
BottomSheetView,
|
|
6
5
|
BottomSheetScrollView,
|
|
7
6
|
BottomSheetBackdrop,
|
|
@@ -149,7 +148,7 @@ export function Sheet({
|
|
|
149
148
|
const { colors } = useTheme()
|
|
150
149
|
const insets = useSafeAreaInsets()
|
|
151
150
|
const { width: windowWidth } = useWindowDimensions()
|
|
152
|
-
const ref = useRef<
|
|
151
|
+
const ref = useRef<BottomSheet>(null)
|
|
153
152
|
const asDialog = responsive && windowWidth >= BREAKPOINTS.wide
|
|
154
153
|
|
|
155
154
|
// 'interactive' + 'adjustPan' works properly with enableDynamicSizing on both platforms
|
|
@@ -159,9 +158,9 @@ export function Sheet({
|
|
|
159
158
|
useEffect(() => {
|
|
160
159
|
if (open) {
|
|
161
160
|
impactMedium()
|
|
162
|
-
ref.current?.
|
|
161
|
+
ref.current?.snapToIndex(0)
|
|
163
162
|
} else {
|
|
164
|
-
ref.current?.
|
|
163
|
+
ref.current?.close()
|
|
165
164
|
}
|
|
166
165
|
}, [open])
|
|
167
166
|
|
|
@@ -193,7 +192,7 @@ export function Sheet({
|
|
|
193
192
|
const showHeader = !!(title || effectiveSubtitle || showCloseButton) && !customHeader
|
|
194
193
|
|
|
195
194
|
const headerNode = customHeader ? customHeader : (showHeader ? (
|
|
196
|
-
<View style={styles.header} accessibilityRole="header">
|
|
195
|
+
<View style={[styles.header, { backgroundColor: colors.card }]} accessibilityRole="header">
|
|
197
196
|
<View style={styles.headerRow}>
|
|
198
197
|
{title ? (
|
|
199
198
|
<Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>
|
|
@@ -270,12 +269,13 @@ export function Sheet({
|
|
|
270
269
|
const useDynamicSizing = !snapPoints
|
|
271
270
|
|
|
272
271
|
return (
|
|
273
|
-
<
|
|
272
|
+
<BottomSheet
|
|
274
273
|
ref={ref}
|
|
274
|
+
index={-1}
|
|
275
|
+
onClose={onClose}
|
|
275
276
|
enableDynamicSizing={useDynamicSizing}
|
|
276
277
|
snapPoints={snapPoints}
|
|
277
278
|
maxDynamicContentSize={useDynamicSizing ? effectiveMaxHeight : undefined}
|
|
278
|
-
onDismiss={onClose}
|
|
279
279
|
backdropComponent={renderBackdrop}
|
|
280
280
|
footerComponent={effectiveFooter ? renderFooter : undefined}
|
|
281
281
|
backgroundStyle={[styles.background, { backgroundColor: colors.card }]}
|
|
@@ -297,6 +297,7 @@ export function Sheet({
|
|
|
297
297
|
showsVerticalScrollIndicator={true}
|
|
298
298
|
indicatorStyle="black"
|
|
299
299
|
persistentScrollbar={isAndroid}
|
|
300
|
+
stickyHeaderIndices={headerNode ? [0] : undefined}
|
|
300
301
|
>
|
|
301
302
|
{headerNode}
|
|
302
303
|
{contentNode}
|
|
@@ -307,7 +308,7 @@ export function Sheet({
|
|
|
307
308
|
{contentNode}
|
|
308
309
|
</BottomSheetView>
|
|
309
310
|
)}
|
|
310
|
-
</
|
|
311
|
+
</BottomSheet>
|
|
311
312
|
)
|
|
312
313
|
}
|
|
313
314
|
|
|
@@ -14,6 +14,8 @@ const THUMB_OFFSET = s(3)
|
|
|
14
14
|
const THUMB_TRAVEL = TRACK_WIDTH - THUMB_SIZE - THUMB_OFFSET * 2
|
|
15
15
|
const ICON_SIZE = s(13)
|
|
16
16
|
|
|
17
|
+
const DISABLED_OPACITY = 0.45
|
|
18
|
+
|
|
17
19
|
export interface SwitchProps {
|
|
18
20
|
checked?: boolean
|
|
19
21
|
onCheckedChange?: (checked: boolean) => void
|
|
@@ -24,9 +26,10 @@ export interface SwitchProps {
|
|
|
24
26
|
|
|
25
27
|
export function Switch({ checked = false, onCheckedChange, disabled, style, accessibilityLabel }: SwitchProps) {
|
|
26
28
|
const { colors } = useTheme()
|
|
29
|
+
const isDisabled = !!disabled
|
|
27
30
|
|
|
28
31
|
return (
|
|
29
|
-
<View style={[{
|
|
32
|
+
<View style={[{ alignSelf: 'flex-start' }, style]}>
|
|
30
33
|
<TouchableOpacity
|
|
31
34
|
onPress={() => {
|
|
32
35
|
hapticSelection()
|
|
@@ -37,20 +40,15 @@ export function Switch({ checked = false, onCheckedChange, disabled, style, acce
|
|
|
37
40
|
touchSoundDisabled={true}
|
|
38
41
|
accessibilityRole="switch"
|
|
39
42
|
accessibilityLabel={accessibilityLabel}
|
|
40
|
-
accessibilityState={{ checked, disabled:
|
|
43
|
+
accessibilityState={{ checked, disabled: isDisabled }}
|
|
41
44
|
style={styles.touchable}
|
|
42
45
|
>
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
AUDIT FIX: the off-state track used surfaceStrong (~#ebebeb in light mode)
|
|
50
|
-
with no border — nearly invisible on white page/card surfaces. A 1.5px border
|
|
51
|
-
that fades out as the track fills gives the off state clear visual definition
|
|
52
|
-
without adding visual weight to the on state.
|
|
53
|
-
*/}
|
|
46
|
+
<View style={styles.trackContainer}>
|
|
47
|
+
<EaseView
|
|
48
|
+
style={[styles.track, isDisabled && styles.disabledTrack]}
|
|
49
|
+
animate={{ backgroundColor: checked ? colors.primary : colors.surfaceStrong }}
|
|
50
|
+
transition={COLOR_TRANSITION}
|
|
51
|
+
/>
|
|
54
52
|
<EaseView
|
|
55
53
|
style={[styles.trackBorder, { borderWidth: 1.5 }]}
|
|
56
54
|
pointerEvents="none"
|
|
@@ -62,14 +60,22 @@ export function Switch({ checked = false, onCheckedChange, disabled, style, acce
|
|
|
62
60
|
animate={{ translateX: checked ? THUMB_TRAVEL : 0 }}
|
|
63
61
|
transition={SPRING_ELASTIC}
|
|
64
62
|
>
|
|
65
|
-
<EaseView
|
|
63
|
+
<EaseView
|
|
64
|
+
style={styles.iconWrapper}
|
|
65
|
+
animate={{ opacity: checked ? (isDisabled ? DISABLED_OPACITY : 1) : 0 }}
|
|
66
|
+
transition={OPACITY_TRANSITION}
|
|
67
|
+
>
|
|
66
68
|
<Feather name="check" size={ICON_SIZE} color={colors.primary} />
|
|
67
69
|
</EaseView>
|
|
68
|
-
<EaseView
|
|
70
|
+
<EaseView
|
|
71
|
+
style={styles.iconWrapper}
|
|
72
|
+
animate={{ opacity: checked ? 0 : (isDisabled ? DISABLED_OPACITY : 1) }}
|
|
73
|
+
transition={OPACITY_TRANSITION}
|
|
74
|
+
>
|
|
69
75
|
<Feather name="x" size={ICON_SIZE} color={colors.foregroundMuted} />
|
|
70
76
|
</EaseView>
|
|
71
77
|
</EaseView>
|
|
72
|
-
</
|
|
78
|
+
</View>
|
|
73
79
|
</TouchableOpacity>
|
|
74
80
|
</View>
|
|
75
81
|
)
|
|
@@ -79,11 +85,18 @@ const styles = StyleSheet.create({
|
|
|
79
85
|
touchable: {
|
|
80
86
|
alignSelf: 'flex-start',
|
|
81
87
|
},
|
|
82
|
-
|
|
88
|
+
trackContainer: {
|
|
89
|
+
position: 'relative',
|
|
83
90
|
width: TRACK_WIDTH,
|
|
84
91
|
height: TRACK_HEIGHT,
|
|
92
|
+
},
|
|
93
|
+
track: {
|
|
94
|
+
...StyleSheet.absoluteFillObject,
|
|
85
95
|
borderRadius: TRACK_HEIGHT / 2,
|
|
86
96
|
},
|
|
97
|
+
disabledTrack: {
|
|
98
|
+
opacity: DISABLED_OPACITY,
|
|
99
|
+
},
|
|
87
100
|
trackBorder: {
|
|
88
101
|
...StyleSheet.absoluteFillObject,
|
|
89
102
|
borderRadius: TRACK_HEIGHT / 2,
|
package/src/fonts.ts
CHANGED
|
@@ -1,53 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sohne font family
|
|
2
|
+
* Sohne font family names for @retray-dev/ui-kit components.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* The postinstall script copies 28 .otf files to your project's assets/fonts/sohne/ folder.
|
|
5
|
+
* You must define SohneFonts with static require() calls in your App.tsx:
|
|
5
6
|
*
|
|
6
7
|
* @example
|
|
7
8
|
* import { useFonts } from 'expo-font'
|
|
8
|
-
*
|
|
9
|
+
*
|
|
10
|
+
* // Fonts copied to assets/fonts/sohne/ by @retray-dev/ui-kit postinstall
|
|
11
|
+
* const SohneFonts = {
|
|
12
|
+
* 'Sohne-ExtraLight': require('./assets/fonts/sohne/Sohne-ExtraLight.otf'),
|
|
13
|
+
* 'Sohne-ExtraLightItalic': require('./assets/fonts/sohne/Sohne-ExtraLightItalic.otf'),
|
|
14
|
+
* // ... see CONSUMER.md for full boilerplate
|
|
15
|
+
* }
|
|
9
16
|
*
|
|
10
17
|
* function App() {
|
|
11
18
|
* const [fontsLoaded] = useFonts(SohneFonts)
|
|
12
19
|
* if (!fontsLoaded) return null
|
|
13
20
|
* // render app
|
|
14
21
|
* }
|
|
22
|
+
*
|
|
23
|
+
* @see CONSUMER.md for the full SohneFonts boilerplate to copy into your App.tsx
|
|
15
24
|
*/
|
|
16
|
-
// `.otf` assets resolve to a Metro asset module id (number) via require() at the
|
|
17
|
-
// consumer's build time. Paths are relative to dist/fonts.js (the compiled output).
|
|
18
|
-
// The build step copies all .otf files into dist/assets/fonts/ so these paths
|
|
19
|
-
// resolve correctly without any ../traversal that Metro would reject.
|
|
20
|
-
declare const require: (path: string) => number
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
'Sohne-
|
|
30
|
-
'Sohne-
|
|
31
|
-
'Sohne-
|
|
32
|
-
'Sohne-
|
|
33
|
-
'Sohne-
|
|
34
|
-
'Sohne-
|
|
35
|
-
'Sohne-
|
|
36
|
-
'Sohne-
|
|
37
|
-
'Sohne-
|
|
38
|
-
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
'
|
|
43
|
-
|
|
44
|
-
'SohneMono-
|
|
45
|
-
'SohneMono-
|
|
46
|
-
'SohneMono-
|
|
47
|
-
'SohneMono-
|
|
48
|
-
'SohneMono-
|
|
49
|
-
'SohneMono-
|
|
50
|
-
'SohneMono-
|
|
51
|
-
'SohneMono-
|
|
52
|
-
'SohneMono-
|
|
53
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Array of all 28 Sohne font family names.
|
|
28
|
+
* Use this for validation or programmatic checks — NOT for loading fonts.
|
|
29
|
+
* To load fonts, use the static require() boilerplate from CONSUMER.md.
|
|
30
|
+
*/
|
|
31
|
+
export const SohneFontNames = [
|
|
32
|
+
// Sohne base (14)
|
|
33
|
+
'Sohne-ExtraLight',
|
|
34
|
+
'Sohne-ExtraLightItalic',
|
|
35
|
+
'Sohne-Light',
|
|
36
|
+
'Sohne-LightItalic',
|
|
37
|
+
'Sohne-Regular',
|
|
38
|
+
'Sohne-Italic',
|
|
39
|
+
'Sohne-Medium',
|
|
40
|
+
'Sohne-MediumItalic',
|
|
41
|
+
'Sohne-SemiBold',
|
|
42
|
+
'Sohne-SemiBoldItalic',
|
|
43
|
+
'Sohne-Bold',
|
|
44
|
+
'Sohne-BoldItalic',
|
|
45
|
+
'Sohne-ExtraBold',
|
|
46
|
+
'Sohne-ExtraBoldItalic',
|
|
47
|
+
// SohneMono (14)
|
|
48
|
+
'SohneMono-ExtraLight',
|
|
49
|
+
'SohneMono-ExtraLightItalic',
|
|
50
|
+
'SohneMono-Light',
|
|
51
|
+
'SohneMono-LightItalic',
|
|
52
|
+
'SohneMono-Regular',
|
|
53
|
+
'SohneMono-Italic',
|
|
54
|
+
'SohneMono-Medium',
|
|
55
|
+
'SohneMono-MediumItalic',
|
|
56
|
+
'SohneMono-SemiBold',
|
|
57
|
+
'SohneMono-SemiBoldItalic',
|
|
58
|
+
'SohneMono-Bold',
|
|
59
|
+
'SohneMono-BoldItalic',
|
|
60
|
+
'SohneMono-ExtraBold',
|
|
61
|
+
'SohneMono-ExtraBoldItalic',
|
|
62
|
+
] as const
|
|
63
|
+
|
|
64
|
+
/** Type for any valid Sohne font family name */
|
|
65
|
+
export type SohneFontName = (typeof SohneFontNames)[number]
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @deprecated SohneFonts export removed in v10.0.0.
|
|
69
|
+
* Metro cannot resolve require() calls from node_modules reliably.
|
|
70
|
+
* Copy the static SohneFonts boilerplate from CONSUMER.md into your App.tsx instead.
|
|
71
|
+
*/
|
|
72
|
+
export const SohneFonts = undefined
|