@studio-piot/native-ui 0.1.0 → 0.2.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 (43) hide show
  1. package/README.md +1 -1
  2. package/dist/components/Select/Select.d.ts.map +1 -1
  3. package/dist/components/Select/Select.js +41 -23
  4. package/dist/components/Select/Select.js.map +1 -1
  5. package/dist/components/Select/SelectCloseButton.d.ts +1 -0
  6. package/dist/components/Select/SelectCloseButton.d.ts.map +1 -1
  7. package/dist/components/Select/SelectCloseButton.js +15 -4
  8. package/dist/components/Select/SelectCloseButton.js.map +1 -1
  9. package/dist/components/Select/index.d.ts +3 -3
  10. package/dist/components/Select/index.d.ts.map +1 -1
  11. package/dist/components/Select/index.js +2 -1
  12. package/dist/components/Select/index.js.map +1 -1
  13. package/dist/components/Select/resolveSelectDetentIndex.d.ts +2 -0
  14. package/dist/components/Select/resolveSelectDetentIndex.d.ts.map +1 -1
  15. package/dist/components/Select/resolveSelectDetentIndex.js +5 -0
  16. package/dist/components/Select/resolveSelectDetentIndex.js.map +1 -1
  17. package/dist/components/Select/trueSheetReanimated.d.ts +13 -0
  18. package/dist/components/Select/trueSheetReanimated.d.ts.map +1 -0
  19. package/dist/components/Select/trueSheetReanimated.js +14 -0
  20. package/dist/components/Select/trueSheetReanimated.js.map +1 -0
  21. package/dist/components/Select/types.d.ts +3 -1
  22. package/dist/components/Select/types.d.ts.map +1 -1
  23. package/dist/components/index.d.ts +1 -1
  24. package/dist/components/index.d.ts.map +1 -1
  25. package/dist/components/index.js +2 -1
  26. package/dist/components/index.js.map +1 -1
  27. package/dist/index.d.ts +2 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +3 -2
  30. package/dist/index.js.map +1 -1
  31. package/docs/components/select.md +26 -9
  32. package/docs/superpowers/plans/2026-09-25-select.md +26 -8
  33. package/docs/superpowers/specs/2026-09-25-select-design.md +37 -65
  34. package/package.json +7 -3
  35. package/src/components/Select/Select.tsx +115 -71
  36. package/src/components/Select/SelectCloseButton.tsx +22 -5
  37. package/src/components/Select/index.ts +6 -2
  38. package/src/components/Select/resolveSelectDetentIndex.test.ts +15 -1
  39. package/src/components/Select/resolveSelectDetentIndex.ts +5 -0
  40. package/src/components/Select/selectThemeRoles.test.ts +16 -0
  41. package/src/components/Select/types.ts +3 -1
  42. package/src/components/index.ts +2 -1
  43. package/src/index.ts +3 -2
@@ -7,22 +7,24 @@ Apps: Ribe create-activity Type field in the same pass
7
7
 
8
8
  ## Goal
9
9
 
10
- Ship a presentational **Select**: Field-like trigger that opens a native bottom sheet of options (TrueSheet). Highlight the selected option; choosing one updates value and dismisses the sheet. Drag handle, dimmed backdrop, and snap heights feel like system UI (Strava-style grabber). Ribe wires Type of activity to Select with a glass close control.
10
+ Ship a presentational **Select**: Field-like trigger that opens a Gorhom bottom sheet of options. Highlight the selected option; choosing one updates value and dismisses the sheet. Grabber, dimmed backdrop, and snap heights match the spike UX. Ribe wires Type of activity to Select; default close is a dark disc with white cross (not glass Cancel).
11
11
 
12
12
  ## Decisions
13
13
 
14
14
  | Topic | Choice |
15
15
  |---|---|
16
16
  | API shape | Single `Select`: trigger + internal sheet |
17
- | Sheet library | `@lodev09/react-native-true-sheet` (native UIKit / Material) |
18
- | Detents | Default `[0.25, 0.5, 0.75]`; present at `0.5` |
19
- | Close control | `closeButton` slot; package default plain Cancel; Ribe passes glass |
17
+ | Sheet library | `@gorhom/bottom-sheet` (`BottomSheetModal`) |
18
+ | Snap points | Default `["25%", "50%", "75%"]` (from detents `[0.25, 0.5, 0.75]`); present at mid (`0.5` / index 1) |
19
+ | Close control | `closeButton` slot; package default **DiscClose** (dark disc + white ×); Ribe uses default |
20
20
  | Colours | Field `field*` roles + `fieldRadioSelected` for selected check / emphasis |
21
21
  | Trigger | Field-like chrome (label, description, error, chevron, radius) |
22
22
  | Options | `{ value, label, disabled? }[]`; scrollable when long |
23
23
  | Select behaviour | Tap option → `onChange(value)` + dismiss |
24
+ | App provider | App must wrap with `GestureHandlerRootView` + `BottomSheetModalProvider` |
24
25
  | Icons / search / grids | Out of scope |
25
- | Optimum migration | Out of scope this pass (CustomPicker remains until a later PR) |
26
+ | TrueSheet | Not used; no TrueSheet peer or native bits |
27
+ | Optimum migration | Out of scope this pass |
26
28
  | Brand coupling | None in the package |
27
29
 
28
30
  ## Architecture
@@ -30,24 +32,25 @@ Ship a presentational **Select**: Field-like trigger that opens a native bottom
30
32
  ```
31
33
  Select
32
34
  ├── trigger (Field-like Pressable)
33
- └── TrueSheet
34
- ├── grabber (system)
35
+ └── BottomSheetModal
36
+ ├── BottomSheetBackdrop (opacity 0.72)
37
+ ├── handle / grabber
35
38
  ├── header (title + closeButton)
36
- └── scrollable options
39
+ └── BottomSheetScrollView options
37
40
  └── row (label + check when selected)
38
41
  ```
39
42
 
40
- - Public export: `Select` (+ types). Default close control may be a small internal component (plain Cancel), not required to be public.
43
+ - Public export: `Select` (+ types). Default close is `SelectCloseButton` (DiscClose).
41
44
  - Controlled only: `value` + `onChange` + `options`.
42
- - Peer dependency: `@lodev09/react-native-true-sheet` (document in usage docs; matching `devDependency` for types/build).
45
+ - Peer dependencies: `@gorhom/bottom-sheet`, `react-native-reanimated`, `react-native-gesture-handler` (plus existing RN peers).
43
46
 
44
47
  ## Theme roles
45
48
 
46
49
  | Usage | Theme role |
47
50
  |---|---|
48
- | Trigger / sheet list ground | `fieldBackground` |
51
+ | Trigger / sheet ground | `fieldBackground` |
49
52
  | Trigger value / option label | `fieldText` |
50
- | Placeholder / chevron | `fieldPlaceholder` |
53
+ | Placeholder / chevron / handle | `fieldPlaceholder` |
51
54
  | Label above trigger | `fieldLabel` |
52
55
  | Description under trigger | `fieldDescription` |
53
56
  | Trigger border / dividers | `fieldBorder` |
@@ -65,6 +68,10 @@ type SelectOption = {
65
68
  disabled?: boolean;
66
69
  };
67
70
 
71
+ type SelectCloseButton =
72
+ | ReactNode
73
+ | ((dismiss: () => void) => ReactNode);
74
+
68
75
  type SelectProps = {
69
76
  value: string | null;
70
77
  onChange: (value: string) => void;
@@ -74,7 +81,7 @@ type SelectProps = {
74
81
  description?: string;
75
82
  error?: string;
76
83
  sheetTitle?: string;
77
- closeButton?: ReactNode;
84
+ closeButton?: SelectCloseButton;
78
85
  detents?: number[];
79
86
  initialDetent?: number;
80
87
  radius?: ButtonRadius;
@@ -88,11 +95,11 @@ type SelectProps = {
88
95
  |---|---|
89
96
  | `value` | Controlled; `null` / unmatched → show placeholder |
90
97
  | `onChange` | Fired with option `value` on row press |
91
- | `options` | Empty list: trigger still opens an empty sheet (or no-op open — prefer open with empty state text “No options”) |
98
+ | `options` | Empty list: open sheet with “No options” empty state |
92
99
  | `sheetTitle` | Header title; default `label` or `"Select"` |
93
- | `closeButton` | Render prop / node for header trailing close; default plain Cancel that dismisses |
94
- | `detents` | Default `[0.25, 0.5, 0.75]` |
95
- | `initialDetent` | Default `0.5` (must be one of `detents`) |
100
+ | `closeButton` | Header trailing close; default DiscClose that dismisses |
101
+ | `detents` | Fractions `0–1`; default `[0.25, 0.5, 0.75]` → Gorhom snap `%` |
102
+ | `initialDetent` | Default `0.5` (must be one of `detents` or falls back to index 0) |
96
103
  | `radius` | Trigger radius; default `"xl"` |
97
104
  | `editable` | `false` → reduced opacity, no open |
98
105
 
@@ -107,58 +114,23 @@ Aligned with Field:
107
114
 
108
115
  ## Sheet behaviour
109
116
 
110
- - Present TrueSheet on trigger press (when `editable`)
111
- - System grabber; dimmed / fading backdrop (library default)
112
- - Header: title + `closeButton` (or default Cancel)
113
- - Scrollable option list; row min height ~52; selected row uses `fieldRadioSelected` for check (and optional label tint)
117
+ - Present `BottomSheetModal` on trigger press (when `editable`)
118
+ - Backdrop opacity `0.72`, press closes
119
+ - Handle indicator; `enablePanDownToClose`
120
+ - Header: title + `closeButton` (or default DiscClose)
121
+ - Scrollable option list; row min height ~52; selected row uses `fieldRadioSelected` for check (and label tint)
114
122
  - Row press: `onChange` + dismiss
115
- - Dismiss also via grabber drag / backdrop / close control
116
- - No search field, no category grids, no leading sport icons in this pass
123
+ - Dismiss also via pan-down / backdrop / close control
117
124
 
118
125
  ## Accessibility
119
126
 
120
- - Trigger: `accessibilityRole="button"`, label from `label` / placeholder; announce current value when set
121
- - Options: `accessibilityRole="button"` (or `menuitem` if we use a menu pattern — prefer button + `accessibilityState.selected`)
122
- - Close control: button with accessible name “Cancel” / “Close”
123
-
124
- ## App work (Ribe)
125
-
126
- 1. `npx expo install @lodev09/react-native-true-sheet` (or pnpm equivalent)
127
- 2. Create-activity: replace Type `FieldText` with `Select`
128
- - Options: Ride, Run, Swim, Walk, Hike (values lowercase slug or same as label — pick stable string values e.g. `ride`, `run`, …)
129
- - `error` when empty (same validation copy intent as today)
130
- - `closeButton` that dismisses the sheet using glass `FormCloseButton` (needs a dismiss callback from Select — either `closeButton` as render prop `(dismiss) => ReactNode` **or** Select clones/passes dismiss via slot convention)
131
-
132
- ### Close slot + dismiss
133
-
134
- Prefer **render prop** so the app can wire glass Cancel correctly:
135
-
136
- ```ts
137
- closeButton?: ReactNode | ((dismiss: () => void) => ReactNode);
138
- ```
139
-
140
- If `ReactNode` only, the default Cancel works but glass Cancel cannot call dismiss unless Select exposes an imperative API. **Decision: support function form** `(dismiss) => ReactNode`, and also accept a static `ReactNode` for simple cases (static node cannot dismiss unless it uses an internal context — for static nodes, wrap with Pressable only in default Cancel).
141
-
142
- Ribe usage:
143
-
144
- ```tsx
145
- closeButton={(dismiss) => <FormCloseButton onPress={dismiss} />}
146
- ```
147
-
148
- 3. Keep Name / Notes as Field; form submit still gated on name (and optionally require type — keep current “type empty → error” UX).
127
+ - Trigger: `accessibilityRole="button"`, label includes field label + current display, hint that it opens options
128
+ - Option rows: `accessibilityRole="button"`, `accessibilityState.selected`
129
+ - Close: `accessibilityLabel="Close"`
149
130
 
150
131
  ## Out of scope
151
132
 
152
- - Strava “Top Sports” horizontal chips / search
153
- - Per-option icons
154
- - Migrating Optimum `CustomPicker` call sites
155
- - Multi-select
156
- - Liquid glass Cancel inside the package
157
- - New `select*` theme roles
158
-
159
- ## Success criteria
160
-
161
- - Package exports `Select` with usage docs and TrueSheet peer documented
162
- - Sheet: grabber, fade backdrop, detents ¼ / ½ / ¾, open at ½, scrollable options, select-to-dismiss
163
- - Ribe create-activity Type uses Select + glass close
164
- - `npm test` / `npm run build` (package) and Ribe `pnpm typecheck` pass
133
+ - Multi-select, search, icons, section headers
134
+ - TrueSheet / RN Modal sheet implementations
135
+ - Migrating Optimum CustomPicker
136
+ - Publishing / EAS workflow changes beyond peers + Ribe wire
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studio-piot/native-ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Shared React Native components for Studio Piot apps.",
5
5
  "homepage": "https://github.com/Studio-Piot/studio-native-ui#readme",
6
6
  "bugs": {
@@ -50,10 +50,12 @@
50
50
  "access": "public"
51
51
  },
52
52
  "peerDependencies": {
53
- "@lodev09/react-native-true-sheet": ">=3.7.0",
53
+ "@gorhom/bottom-sheet": "^5.2.14",
54
54
  "react": ">=19.0.0",
55
55
  "react-native": ">=0.76.0",
56
+ "react-native-gesture-handler": "^2.32.0",
56
57
  "react-native-keyboard-controller": ">=1.14.0",
58
+ "react-native-reanimated": "^4.5.1",
57
59
  "react-native-safe-area-context": ">=4.0.0"
58
60
  },
59
61
  "scripts": {
@@ -65,10 +67,12 @@
65
67
  "prepublishOnly": "npm run build"
66
68
  },
67
69
  "devDependencies": {
68
- "@lodev09/react-native-true-sheet": "3.11.15",
70
+ "@gorhom/bottom-sheet": "^5.2.14",
69
71
  "@types/react": "^19.2.2",
70
72
  "react-native": "^0.86.3",
73
+ "react-native-gesture-handler": "^2.32.0",
71
74
  "react-native-keyboard-controller": "1.21.9",
75
+ "react-native-reanimated": "^4.5.1",
72
76
  "react-native-safe-area-context": "~5.7.0",
73
77
  "typescript": "^5.9.3"
74
78
  }
@@ -1,17 +1,31 @@
1
- import { useCallback, useRef, type ReactNode } from "react";
1
+ import {
2
+ useCallback,
3
+ useMemo,
4
+ useRef,
5
+ type ElementRef,
6
+ type ReactNode,
7
+ } from "react";
2
8
  import { Pressable, StyleSheet, Text, View } from "react-native";
3
- import { TrueSheet } from "@lodev09/react-native-true-sheet";
9
+ import {
10
+ BottomSheetBackdrop,
11
+ BottomSheetModal,
12
+ BottomSheetScrollView,
13
+ type BottomSheetBackdropProps,
14
+ } from "@gorhom/bottom-sheet";
4
15
  import { buttonRadiusValue } from "../Button/buttonRadius";
5
16
  import { useResolvedFieldColor } from "../Field/useResolvedFieldColor";
6
17
  import {
7
18
  DEFAULT_SELECT_DETENTS,
8
19
  DEFAULT_SELECT_INITIAL_DETENT,
20
+ detentsToSnapPoints,
9
21
  resolveSelectDetentIndex,
10
22
  } from "./resolveSelectDetentIndex";
11
23
  import { SelectCloseButton } from "./SelectCloseButton";
12
24
  import { SELECT_THEME_ROLES } from "./selectThemeRoles";
13
25
  import type { SelectOption, SelectProps } from "./types";
14
26
 
27
+ const BACKDROP_OPACITY = 0.72;
28
+
15
29
  export function Select({
16
30
  value,
17
31
  onChange,
@@ -36,7 +50,7 @@ export function Select({
36
50
  selectedColor: selectedColorProp,
37
51
  style,
38
52
  }: SelectProps) {
39
- const sheetRef = useRef<TrueSheet>(null);
53
+ const modalRef = useRef<ElementRef<typeof BottomSheetModal>>(null);
40
54
 
41
55
  const backgroundColor = useResolvedFieldColor(
42
56
  backgroundColorProp,
@@ -78,15 +92,29 @@ export function Select({
78
92
  const borderRadius = buttonRadiusValue(radius);
79
93
  const title = sheetTitle ?? label ?? "Select";
80
94
  const presentIndex = resolveSelectDetentIndex(detents, initialDetent);
95
+ const snapPoints = useMemo(() => detentsToSnapPoints(detents), [detents]);
81
96
 
82
97
  const dismiss = useCallback(() => {
83
- void sheetRef.current?.dismiss();
98
+ modalRef.current?.dismiss();
84
99
  }, []);
85
100
 
86
101
  const present = useCallback(() => {
87
102
  if (!editable) return;
88
- void sheetRef.current?.present(presentIndex);
89
- }, [editable, presentIndex]);
103
+ modalRef.current?.present();
104
+ }, [editable]);
105
+
106
+ const renderBackdrop = useCallback(
107
+ (props: BottomSheetBackdropProps) => (
108
+ <BottomSheetBackdrop
109
+ {...props}
110
+ appearsOnIndex={0}
111
+ disappearsOnIndex={-1}
112
+ opacity={BACKDROP_OPACITY}
113
+ pressBehavior="close"
114
+ />
115
+ ),
116
+ [],
117
+ );
90
118
 
91
119
  const resolvedClose: ReactNode =
92
120
  typeof closeButton === "function"
@@ -145,68 +173,75 @@ export function Select({
145
173
  </Text>
146
174
  ) : null}
147
175
 
148
- <TrueSheet
149
- ref={sheetRef}
150
- backgroundColor={backgroundColor}
151
- cornerRadius={buttonRadiusValue("xl")}
152
- detents={detents}
153
- grabber
154
- scrollable
176
+ <BottomSheetModal
177
+ ref={modalRef}
178
+ backdropComponent={renderBackdrop}
179
+ backgroundStyle={{ backgroundColor }}
180
+ enablePanDownToClose
181
+ handleIndicatorStyle={{
182
+ backgroundColor: placeholderColor,
183
+ opacity: 0.9,
184
+ }}
185
+ index={presentIndex}
186
+ snapPoints={snapPoints}
155
187
  >
156
- <View style={styles.sheetHeader}>
157
- <Text
158
- accessibilityRole="header"
159
- numberOfLines={1}
160
- style={[styles.sheetTitle, { color: textColor, flex: 1 }]}
161
- >
162
- {title}
163
- </Text>
164
- {resolvedClose}
165
- </View>
166
- {options.length === 0 ? (
167
- <Text style={[styles.empty, { color: placeholderColor }]}>
168
- No options
169
- </Text>
170
- ) : (
171
- options.map((item) => {
172
- const isSelected = item.value === value;
173
- return (
174
- <Pressable
175
- key={item.value}
176
- accessibilityRole="button"
177
- accessibilityState={{
178
- selected: isSelected,
179
- disabled: !!item.disabled,
180
- }}
181
- disabled={item.disabled}
182
- onPress={() => onSelect(item)}
183
- style={[styles.option, item.disabled && styles.blocked]}
184
- >
185
- <Text
186
- style={[
187
- styles.optionLabel,
188
- {
189
- color: isSelected ? selectedColor : textColor,
190
- flex: 1,
191
- },
192
- ]}
193
- >
194
- {item.label}
195
- </Text>
196
- {isSelected ? (
197
- <Text
198
- accessibilityElementsHidden
199
- importantForAccessibility="no"
200
- style={[styles.check, { color: selectedColor }]}
188
+ <BottomSheetScrollView contentContainerStyle={styles.sheetContent}>
189
+ <View style={styles.header}>
190
+ <Text style={[styles.sheetTitle, { color: textColor, flex: 1 }]}>
191
+ {title}
192
+ </Text>
193
+ {resolvedClose}
194
+ </View>
195
+ {options.length === 0 ? (
196
+ <Text style={[styles.empty, { color: placeholderColor }]}>
197
+ No options
198
+ </Text>
199
+ ) : (
200
+ options.map((option, index) => {
201
+ const isSelected = option.value === value;
202
+ return (
203
+ <View key={option.value}>
204
+ {index > 0 ? (
205
+ <View
206
+ style={[styles.divider, { backgroundColor: borderColor }]}
207
+ />
208
+ ) : null}
209
+ <Pressable
210
+ accessibilityRole="button"
211
+ accessibilityState={{
212
+ selected: isSelected,
213
+ disabled: !!option.disabled,
214
+ }}
215
+ disabled={option.disabled}
216
+ onPress={() => onSelect(option)}
217
+ style={[
218
+ styles.option,
219
+ option.disabled && styles.optionBlocked,
220
+ ]}
201
221
  >
202
- ✓
203
- </Text>
204
- ) : null}
205
- </Pressable>
206
- );
207
- })
208
- )}
209
- </TrueSheet>
222
+ <Text
223
+ style={[
224
+ styles.optionLabel,
225
+ {
226
+ color: isSelected ? selectedColor : textColor,
227
+ flex: 1,
228
+ },
229
+ ]}
230
+ >
231
+ {option.label}
232
+ </Text>
233
+ {isSelected ? (
234
+ <Text style={[styles.check, { color: selectedColor }]}>
235
+ ✓
236
+ </Text>
237
+ ) : null}
238
+ </Pressable>
239
+ </View>
240
+ );
241
+ })
242
+ )}
243
+ </BottomSheetScrollView>
244
+ </BottomSheetModal>
210
245
  </View>
211
246
  );
212
247
  }
@@ -225,6 +260,7 @@ const styles = StyleSheet.create({
225
260
  },
226
261
  trigger: {
227
262
  minHeight: 48,
263
+ width: "100%",
228
264
  borderWidth: 1,
229
265
  paddingHorizontal: 14,
230
266
  paddingVertical: 12,
@@ -239,12 +275,14 @@ const styles = StyleSheet.create({
239
275
  fontSize: 22,
240
276
  fontWeight: "300",
241
277
  lineHeight: 24,
242
- transform: [{ rotate: "90deg" }],
243
278
  },
244
279
  helper: {
245
280
  fontSize: 13,
246
281
  },
247
- sheetHeader: {
282
+ sheetContent: {
283
+ paddingBottom: 24,
284
+ },
285
+ header: {
248
286
  flexDirection: "row",
249
287
  alignItems: "center",
250
288
  gap: 12,
@@ -257,10 +295,9 @@ const styles = StyleSheet.create({
257
295
  fontWeight: "600",
258
296
  },
259
297
  empty: {
260
- fontSize: 17,
298
+ fontSize: 15,
261
299
  paddingHorizontal: 16,
262
- paddingVertical: 24,
263
- textAlign: "center",
300
+ paddingVertical: 20,
264
301
  },
265
302
  option: {
266
303
  minHeight: 52,
@@ -269,6 +306,9 @@ const styles = StyleSheet.create({
269
306
  alignItems: "center",
270
307
  gap: 12,
271
308
  },
309
+ optionBlocked: {
310
+ opacity: 0.4,
311
+ },
272
312
  optionLabel: {
273
313
  fontSize: 17,
274
314
  },
@@ -276,4 +316,8 @@ const styles = StyleSheet.create({
276
316
  fontSize: 17,
277
317
  fontWeight: "600",
278
318
  },
319
+ divider: {
320
+ height: StyleSheet.hairlineWidth,
321
+ marginHorizontal: 16,
322
+ },
279
323
  });
@@ -1,19 +1,36 @@
1
1
  import { Pressable, StyleSheet, Text } from "react-native";
2
2
  import { useThemeColor } from "../../theme/useThemeColor";
3
3
 
4
+ /** Default Select sheet close: filled disc + contrasting cross. */
4
5
  export function SelectCloseButton({ onPress }: { onPress: () => void }) {
5
- const text = useThemeColor("text");
6
+ const disc = useThemeColor("text");
7
+ const cross = useThemeColor("background");
6
8
 
7
9
  return (
8
- <Pressable accessibilityRole="button" hitSlop={8} onPress={onPress}>
9
- <Text style={[styles.label, { color: text }]}>Cancel</Text>
10
+ <Pressable
11
+ accessibilityLabel="Close"
12
+ accessibilityRole="button"
13
+ hitSlop={8}
14
+ onPress={onPress}
15
+ style={[styles.disc, { backgroundColor: disc }]}
16
+ >
17
+ <Text style={[styles.discX, { color: cross }]}>×</Text>
10
18
  </Pressable>
11
19
  );
12
20
  }
13
21
 
14
22
  const styles = StyleSheet.create({
15
- label: {
16
- fontSize: 17,
23
+ disc: {
24
+ width: 28,
25
+ height: 28,
26
+ borderRadius: 14,
27
+ alignItems: "center",
28
+ justifyContent: "center",
29
+ },
30
+ discX: {
31
+ fontSize: 20,
17
32
  fontWeight: "600",
33
+ lineHeight: 22,
34
+ marginTop: -1,
18
35
  },
19
36
  });
@@ -3,11 +3,15 @@ export { SelectCloseButton } from "./SelectCloseButton";
3
3
  export {
4
4
  DEFAULT_SELECT_DETENTS,
5
5
  DEFAULT_SELECT_INITIAL_DETENT,
6
+ detentsToSnapPoints,
6
7
  resolveSelectDetentIndex,
7
8
  } from "./resolveSelectDetentIndex";
8
- export { SELECT_THEME_ROLES, type SelectThemeColorProp } from "./selectThemeRoles";
9
+ export {
10
+ SELECT_THEME_ROLES,
11
+ type SelectThemeColorProp,
12
+ } from "./selectThemeRoles";
9
13
  export type {
10
- SelectCloseButton as SelectCloseButtonProp,
14
+ SelectCloseButton as SelectCloseButtonSlot,
11
15
  SelectOption,
12
16
  SelectProps,
13
17
  } from "./types";
@@ -3,13 +3,17 @@ import { describe, it } from "node:test";
3
3
  import {
4
4
  DEFAULT_SELECT_DETENTS,
5
5
  DEFAULT_SELECT_INITIAL_DETENT,
6
+ detentsToSnapPoints,
6
7
  resolveSelectDetentIndex,
7
8
  } from "./resolveSelectDetentIndex.ts";
8
9
 
9
10
  describe("resolveSelectDetentIndex", () => {
10
11
  it("returns the index of the initial detent", () => {
11
12
  assert.equal(
12
- resolveSelectDetentIndex(DEFAULT_SELECT_DETENTS, DEFAULT_SELECT_INITIAL_DETENT),
13
+ resolveSelectDetentIndex(
14
+ DEFAULT_SELECT_DETENTS,
15
+ DEFAULT_SELECT_INITIAL_DETENT,
16
+ ),
13
17
  1,
14
18
  );
15
19
  });
@@ -18,3 +22,13 @@ describe("resolveSelectDetentIndex", () => {
18
22
  assert.equal(resolveSelectDetentIndex([0.25, 0.5, 0.75], 0.9), 0);
19
23
  });
20
24
  });
25
+
26
+ describe("detentsToSnapPoints", () => {
27
+ it("maps fractions to percentage snap points", () => {
28
+ assert.deepEqual(detentsToSnapPoints(DEFAULT_SELECT_DETENTS), [
29
+ "25%",
30
+ "50%",
31
+ "75%",
32
+ ]);
33
+ });
34
+ });
@@ -14,3 +14,8 @@ export function resolveSelectDetentIndex(
14
14
  const index = detents.indexOf(initial);
15
15
  return index >= 0 ? index : 0;
16
16
  }
17
+
18
+ /** Convert detent fractions (0–1) to Gorhom snap-point percentage strings. */
19
+ export function detentsToSnapPoints(detents: readonly number[]): string[] {
20
+ return detents.map((detent) => `${Math.round(detent * 100)}%`);
21
+ }
@@ -0,0 +1,16 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { SELECT_THEME_ROLES } from "./selectThemeRoles.ts";
4
+
5
+ describe("SELECT_THEME_ROLES", () => {
6
+ it("maps colour props to Field roles plus fieldRadioSelected", () => {
7
+ assert.equal(SELECT_THEME_ROLES.backgroundColor, "fieldBackground");
8
+ assert.equal(SELECT_THEME_ROLES.textColor, "fieldText");
9
+ assert.equal(SELECT_THEME_ROLES.placeholderColor, "fieldPlaceholder");
10
+ assert.equal(SELECT_THEME_ROLES.labelColor, "fieldLabel");
11
+ assert.equal(SELECT_THEME_ROLES.descriptionColor, "fieldDescription");
12
+ assert.equal(SELECT_THEME_ROLES.borderColor, "fieldBorder");
13
+ assert.equal(SELECT_THEME_ROLES.errorColor, "fieldError");
14
+ assert.equal(SELECT_THEME_ROLES.selectedColor, "fieldRadioSelected");
15
+ });
16
+ });
@@ -21,9 +21,11 @@ export type SelectProps = {
21
21
  description?: string;
22
22
  error?: string;
23
23
  sheetTitle?: string;
24
- /** Plain Cancel by default. Pass `(dismiss) => <Glass onPress={dismiss} />` for app chrome. */
24
+ /** DiscClose by default. Pass `(dismiss) => <YourClose onPress={dismiss} />` to override. */
25
25
  closeButton?: SelectCloseButton;
26
+ /** Detent fractions 0–1; mapped to Gorhom snap `%`. Default `[0.25, 0.5, 0.75]`. */
26
27
  detents?: number[];
28
+ /** Which detent to open at. Default `0.5`. */
27
29
  initialDetent?: number;
28
30
  radius?: ButtonRadius;
29
31
  editable?: boolean;
@@ -34,9 +34,10 @@ export {
34
34
  SELECT_THEME_ROLES,
35
35
  DEFAULT_SELECT_DETENTS,
36
36
  DEFAULT_SELECT_INITIAL_DETENT,
37
+ detentsToSnapPoints,
37
38
  resolveSelectDetentIndex,
38
39
  type SelectThemeColorProp,
40
+ type SelectCloseButtonSlot,
39
41
  type SelectOption,
40
42
  type SelectProps,
41
- type SelectCloseButtonProp,
42
43
  } from "./Select";
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export const version = "0.1.0";
1
+ export const version = "0.2.0";
2
2
 
3
3
  export * from "./theme";
4
4
  export {
@@ -41,10 +41,11 @@ export {
41
41
  SELECT_THEME_ROLES,
42
42
  DEFAULT_SELECT_DETENTS,
43
43
  DEFAULT_SELECT_INITIAL_DETENT,
44
+ detentsToSnapPoints,
44
45
  resolveSelectDetentIndex,
45
46
  type SelectThemeColorProp,
47
+ type SelectCloseButtonSlot,
46
48
  type SelectOption,
47
49
  type SelectProps,
48
- type SelectCloseButtonProp,
49
50
  } from "./components/Select";
50
51
  export * as components from "./components";