@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.
Files changed (39) hide show
  1. package/COMPONENTS.md +136 -22
  2. package/CONSUMER.md +48 -8
  3. package/FONTS.md +54 -13
  4. package/README.md +40 -3
  5. package/dist/Accordion.d.mts +1 -1
  6. package/dist/Accordion.d.ts +1 -1
  7. package/dist/Accordion.js +2 -2
  8. package/dist/Accordion.mjs +1 -1
  9. package/dist/ConfirmDialog.d.mts +6 -1
  10. package/dist/ConfirmDialog.d.ts +6 -1
  11. package/dist/ConfirmDialog.js +44 -14
  12. package/dist/ConfirmDialog.mjs +1 -1
  13. package/dist/ImageViewer.js +282 -141
  14. package/dist/ImageViewer.mjs +3 -1
  15. package/dist/Sheet.js +16 -13
  16. package/dist/Sheet.mjs +1 -1
  17. package/dist/Switch.js +40 -17
  18. package/dist/Switch.mjs +1 -1
  19. package/dist/{chunk-O3HA6TYM.mjs → chunk-DJ7RN37L.mjs} +2 -2
  20. package/dist/{chunk-FZZLPJ6B.mjs → chunk-KZL5VTYK.mjs} +43 -14
  21. package/dist/{chunk-QKH5ZOD5.mjs → chunk-WF2XDFRK.mjs} +40 -17
  22. package/dist/{chunk-Z4BVUWW6.mjs → chunk-WOEYDUJZ.mjs} +19 -31
  23. package/dist/{chunk-PFZTM6D5.mjs → chunk-Y2NS74WS.mjs} +9 -7
  24. package/dist/fonts.d.mts +39 -31
  25. package/dist/fonts.d.ts +39 -31
  26. package/dist/fonts.js +34 -39
  27. package/dist/fonts.mjs +35 -34
  28. package/dist/index.js +119 -76
  29. package/dist/index.mjs +5 -5
  30. package/package.json +3 -1
  31. package/scripts/build-apk.sh +84 -0
  32. package/scripts/copy-fonts.js +90 -0
  33. package/scripts/test-consumer-fonts.sh +82 -0
  34. package/src/components/Accordion/Accordion.tsx +7 -3
  35. package/src/components/ConfirmDialog/ConfirmDialog.tsx +61 -23
  36. package/src/components/ImageViewer/ImageViewer.tsx +25 -30
  37. package/src/components/Sheet/Sheet.tsx +10 -9
  38. package/src/components/Switch/Switch.tsx +30 -17
  39. 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
- <Text style={[styles.triggerText, { color: colors.foreground }]} allowFontScaling={true}>{item.trigger}</Text>
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<BottomSheetModal>(null)
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?.present()
52
+ ref.current?.snapToIndex(0)
46
53
  } else {
47
- ref.current?.dismiss()
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
- <BottomSheetModal
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
- <Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>
72
- {title}
73
- </Text>
74
- {description ? (
75
- <Text style={[styles.description, { color: colors.foregroundMuted }]} allowFontScaling={true}>
76
- {description}
77
- </Text>
78
- ) : null}
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
- </BottomSheetModal>
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(24),
148
+ paddingHorizontal: s(16),
124
149
  paddingBottom: vs(32),
125
- gap: vs(12),
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
- lineHeight: mvs(26),
164
+ flex: 1,
165
+ },
166
+ closeButton: {
167
+ padding: s(4),
168
+ marginLeft: s(8),
131
169
  },
132
- description: {
170
+ subtitle: {
133
171
  fontFamily: 'Sohne-Regular',
134
- fontSize: ms(15),
135
- lineHeight: mvs(22),
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
- TouchableOpacity,
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 { renderIcon } from '../../utils/icons'
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
- <Animated.View style={[{ width, height }, styles.imageWrap]}>
106
- <Animated.Image
107
- source={source}
108
- style={[{ width, height }, animatedStyle]}
109
- resizeMode="contain"
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 { width, height } = useWindowDimensions()
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
- <Animated.View style={styles.root}>
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
- </Animated.View>
227
+ </View>
227
228
  </GestureDetector>
228
229
 
229
- <TouchableOpacity
230
- style={[styles.closeButton, { top: insets.top + vs(8) }]}
231
- onPress={onClose}
232
- activeOpacity={0.7}
233
- touchSoundDisabled={true}
234
- accessibilityRole="button"
235
- accessibilityLabel="Close"
236
- hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
237
- >
238
- {renderIcon('x', 26, '#fff')}
239
- </TouchableOpacity>
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
- closeButton: {
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<BottomSheetModal>(null)
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?.present()
161
+ ref.current?.snapToIndex(0)
163
162
  } else {
164
- ref.current?.dismiss()
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
- <BottomSheetModal
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
- </BottomSheetModal>
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={[{ opacity: disabled ? 0.45 : 1, alignSelf: 'flex-start' }, 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: !!disabled }}
43
+ accessibilityState={{ checked, disabled: isDisabled }}
41
44
  style={styles.touchable}
42
45
  >
43
- <EaseView
44
- style={styles.track}
45
- animate={{ backgroundColor: checked ? colors.primary : colors.surfaceStrong }}
46
- transition={COLOR_TRANSITION}
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 style={styles.iconWrapper} animate={{ opacity: checked ? 1 : 0 }} transition={OPACITY_TRANSITION}>
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 style={styles.iconWrapper} animate={{ opacity: checked ? 0 : 1 }} transition={OPACITY_TRANSITION}>
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
- </EaseView>
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
- track: {
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 required by @retray-dev/ui-kit components.
2
+ * Sohne font family names for @retray-dev/ui-kit components.
3
3
  *
4
- * Consumer apps must load these fonts at app root using expo-font:
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
- * import { SohneFonts } from '@retray-dev/ui-kit/fonts'
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
- export const SohneFonts = {
23
- // Sohne base
24
- 'Sohne-ExtraLight': require('./assets/fonts/Sohne-ExtraLight.otf'),
25
- 'Sohne-ExtraLightItalic': require('./assets/fonts/Sohne-ExtraLightItalic.otf'),
26
- 'Sohne-Light': require('./assets/fonts/Sohne-Light.otf'),
27
- 'Sohne-LightItalic': require('./assets/fonts/Sohne-LightItalic.otf'),
28
- 'Sohne-Regular': require('./assets/fonts/Sohne-Regular.otf'),
29
- 'Sohne-Italic': require('./assets/fonts/Sohne-Italic.otf'),
30
- 'Sohne-Medium': require('./assets/fonts/Sohne-Medium.otf'),
31
- 'Sohne-MediumItalic': require('./assets/fonts/Sohne-MediumItalic.otf'),
32
- 'Sohne-SemiBold': require('./assets/fonts/Sohne-SemiBold.otf'),
33
- 'Sohne-SemiBoldItalic': require('./assets/fonts/Sohne-SemiBoldItalic.otf'),
34
- 'Sohne-Bold': require('./assets/fonts/Sohne-Bold.otf'),
35
- 'Sohne-BoldItalic': require('./assets/fonts/Sohne-BoldItalic.otf'),
36
- 'Sohne-ExtraBold': require('./assets/fonts/Sohne-ExtraBold.otf'),
37
- 'Sohne-ExtraBoldItalic': require('./assets/fonts/Sohne-ExtraBoldItalic.otf'),
38
- // SohneMono
39
- 'SohneMono-ExtraLight': require('./assets/fonts/SohneMono-ExtraLight.otf'),
40
- 'SohneMono-ExtraLightItalic': require('./assets/fonts/SohneMono-ExtraLightItalic.otf'),
41
- 'SohneMono-Light': require('./assets/fonts/SohneMono-Light.otf'),
42
- 'SohneMono-LightItalic': require('./assets/fonts/SohneMono-LightItalic.otf'),
43
- 'SohneMono-Regular': require('./assets/fonts/SohneMono-Regular.otf'),
44
- 'SohneMono-Italic': require('./assets/fonts/SohneMono-Italic.otf'),
45
- 'SohneMono-Medium': require('./assets/fonts/SohneMono-Medium.otf'),
46
- 'SohneMono-MediumItalic': require('./assets/fonts/SohneMono-MediumItalic.otf'),
47
- 'SohneMono-SemiBold': require('./assets/fonts/SohneMono-SemiBold.otf'),
48
- 'SohneMono-SemiBoldItalic': require('./assets/fonts/SohneMono-SemiBoldItalic.otf'),
49
- 'SohneMono-Bold': require('./assets/fonts/SohneMono-Bold.otf'),
50
- 'SohneMono-BoldItalic': require('./assets/fonts/SohneMono-BoldItalic.otf'),
51
- 'SohneMono-ExtraBold': require('./assets/fonts/SohneMono-ExtraBold.otf'),
52
- 'SohneMono-ExtraBoldItalic': require('./assets/fonts/SohneMono-ExtraBoldItalic.otf'),
53
- } as const
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