@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.
- package/README.md +1 -1
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Select/Select.js +41 -23
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Select/SelectCloseButton.d.ts +1 -0
- package/dist/components/Select/SelectCloseButton.d.ts.map +1 -1
- package/dist/components/Select/SelectCloseButton.js +15 -4
- package/dist/components/Select/SelectCloseButton.js.map +1 -1
- package/dist/components/Select/index.d.ts +3 -3
- package/dist/components/Select/index.d.ts.map +1 -1
- package/dist/components/Select/index.js +2 -1
- package/dist/components/Select/index.js.map +1 -1
- package/dist/components/Select/resolveSelectDetentIndex.d.ts +2 -0
- package/dist/components/Select/resolveSelectDetentIndex.d.ts.map +1 -1
- package/dist/components/Select/resolveSelectDetentIndex.js +5 -0
- package/dist/components/Select/resolveSelectDetentIndex.js.map +1 -1
- package/dist/components/Select/trueSheetReanimated.d.ts +13 -0
- package/dist/components/Select/trueSheetReanimated.d.ts.map +1 -0
- package/dist/components/Select/trueSheetReanimated.js +14 -0
- package/dist/components/Select/trueSheetReanimated.js.map +1 -0
- package/dist/components/Select/types.d.ts +3 -1
- package/dist/components/Select/types.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -1
- package/dist/components/index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/docs/components/select.md +26 -9
- package/docs/superpowers/plans/2026-09-25-select.md +26 -8
- package/docs/superpowers/specs/2026-09-25-select-design.md +37 -65
- package/package.json +7 -3
- package/src/components/Select/Select.tsx +115 -71
- package/src/components/Select/SelectCloseButton.tsx +22 -5
- package/src/components/Select/index.ts +6 -2
- package/src/components/Select/resolveSelectDetentIndex.test.ts +15 -1
- package/src/components/Select/resolveSelectDetentIndex.ts +5 -0
- package/src/components/Select/selectThemeRoles.test.ts +16 -0
- package/src/components/Select/types.ts +3 -1
- package/src/components/index.ts +2 -1
- 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
|
|
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 | `@
|
|
18
|
-
|
|
|
19
|
-
| Close control | `closeButton` slot; package default
|
|
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
|
-
|
|
|
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
|
-
└──
|
|
34
|
-
├──
|
|
35
|
+
└── BottomSheetModal
|
|
36
|
+
├── BottomSheetBackdrop (opacity 0.72)
|
|
37
|
+
├── handle / grabber
|
|
35
38
|
├── header (title + closeButton)
|
|
36
|
-
└──
|
|
39
|
+
└── BottomSheetScrollView options
|
|
37
40
|
└── row (label + check when selected)
|
|
38
41
|
```
|
|
39
42
|
|
|
40
|
-
- Public export: `Select` (+ types). Default close
|
|
43
|
+
- Public export: `Select` (+ types). Default close is `SelectCloseButton` (DiscClose).
|
|
41
44
|
- Controlled only: `value` + `onChange` + `options`.
|
|
42
|
-
- Peer
|
|
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
|
|
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?:
|
|
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:
|
|
98
|
+
| `options` | Empty list: open sheet with “No options” empty state |
|
|
92
99
|
| `sheetTitle` | Header title; default `label` or `"Select"` |
|
|
93
|
-
| `closeButton` |
|
|
94
|
-
| `detents` |
|
|
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
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
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
|
|
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
|
|
121
|
-
-
|
|
122
|
-
- 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
|
-
-
|
|
153
|
-
-
|
|
154
|
-
- Migrating Optimum
|
|
155
|
-
-
|
|
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.
|
|
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
|
-
"@
|
|
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
|
-
"@
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
98
|
+
modalRef.current?.dismiss();
|
|
84
99
|
}, []);
|
|
85
100
|
|
|
86
101
|
const present = useCallback(() => {
|
|
87
102
|
if (!editable) return;
|
|
88
|
-
|
|
89
|
-
}, [editable
|
|
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
|
-
<
|
|
149
|
-
ref={
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
<
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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:
|
|
298
|
+
fontSize: 15,
|
|
261
299
|
paddingHorizontal: 16,
|
|
262
|
-
paddingVertical:
|
|
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
|
|
6
|
+
const disc = useThemeColor("text");
|
|
7
|
+
const cross = useThemeColor("background");
|
|
6
8
|
|
|
7
9
|
return (
|
|
8
|
-
<Pressable
|
|
9
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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 {
|
|
9
|
+
export {
|
|
10
|
+
SELECT_THEME_ROLES,
|
|
11
|
+
type SelectThemeColorProp,
|
|
12
|
+
} from "./selectThemeRoles";
|
|
9
13
|
export type {
|
|
10
|
-
SelectCloseButton as
|
|
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(
|
|
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
|
-
/**
|
|
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;
|
package/src/components/index.ts
CHANGED
|
@@ -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
|
+
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";
|