@shaquillehinds/react-native-bottom-sheet 0.0.6 → 0.0.7

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.
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const PACKAGE_NAME = '@shaquillehinds/react-native-bottom-sheet';
8
+ const SLUG = 'react-native-bottom-sheet';
9
+ const SOURCE = path.join(__dirname, '..', 'rules', 'AGENT_RULES.md');
10
+
11
+ const TARGETS = {
12
+ agents: 'AGENTS.md',
13
+ cursor: path.join('.cursor', 'rules', SLUG + '.mdc'),
14
+ claude: path.join('.claude', 'rules', SLUG + '.md'),
15
+ codex: path.join('.codex', 'rules', SLUG + '.md'),
16
+ copilot: path.join('.github', 'instructions', SLUG + '.instructions.md'),
17
+ windsurf: path.join('.windsurf', 'rules', SLUG + '.md'),
18
+ };
19
+
20
+ const HELP = `
21
+ rnbs-rules — install the ${PACKAGE_NAME} agent rules
22
+
23
+ Usage
24
+ npx rnbs-rules [target|path] [--force] [--print]
25
+
26
+ Targets
27
+ (none) ./AGENTS.md
28
+ agents ./AGENTS.md
29
+ cursor ./.cursor/rules/${SLUG}.mdc (alwaysApply)
30
+ claude ./.claude/rules/${SLUG}.md
31
+ codex ./.codex/rules/${SLUG}.md
32
+ copilot ./.github/instructions/${SLUG}.instructions.md
33
+ windsurf ./.windsurf/rules/${SLUG}.md
34
+ <path> any path ending in .md or .mdc
35
+
36
+ Options
37
+ --force overwrite an existing file
38
+ --print write nothing, print the rules to stdout
39
+ --help show this message
40
+
41
+ Examples
42
+ npx rnbs-rules
43
+ npx rnbs-rules cursor
44
+ npx rnbs-rules docs/ai/bottom-sheet.md --force
45
+ `;
46
+
47
+ function frontmatterFor(target) {
48
+ if (target === 'cursor') {
49
+ return [
50
+ '---',
51
+ `description: How to use ${PACKAGE_NAME} correctly`,
52
+ 'globs:',
53
+ 'alwaysApply: true',
54
+ '---',
55
+ '',
56
+ '',
57
+ ].join('\n');
58
+ }
59
+ if (target === 'copilot') {
60
+ return ['---', "applyTo: '**/*.tsx,**/*.ts'", '---', '', ''].join('\n');
61
+ }
62
+ if (target === 'windsurf') {
63
+ return ['---', 'trigger: always_on', '---', '', ''].join('\n');
64
+ }
65
+ return '';
66
+ }
67
+
68
+ function resolveTarget(arg) {
69
+ if (!arg) return { target: 'agents', relPath: TARGETS.agents };
70
+ if (Object.prototype.hasOwnProperty.call(TARGETS, arg)) {
71
+ return { target: arg, relPath: TARGETS[arg] };
72
+ }
73
+ if (/\.mdx?$|\.mdc$/.test(arg)) {
74
+ return { target: 'custom', relPath: arg };
75
+ }
76
+ return null;
77
+ }
78
+
79
+ function main() {
80
+ const argv = process.argv.slice(2);
81
+ const force = argv.includes('--force') || argv.includes('-f');
82
+ const print = argv.includes('--print');
83
+ const help = argv.includes('--help') || argv.includes('-h');
84
+ const positional = argv.filter((a) => !a.startsWith('-'))[0];
85
+
86
+ if (help) {
87
+ process.stdout.write(HELP);
88
+ return;
89
+ }
90
+
91
+ let rules;
92
+ try {
93
+ rules = fs.readFileSync(SOURCE, 'utf8');
94
+ } catch (err) {
95
+ console.error(
96
+ `rnbs-rules: could not read the rules file at ${SOURCE}\n` +
97
+ `Is ${PACKAGE_NAME} installed?`
98
+ );
99
+ process.exitCode = 1;
100
+ return;
101
+ }
102
+
103
+ if (print) {
104
+ process.stdout.write(rules);
105
+ return;
106
+ }
107
+
108
+ const resolved = resolveTarget(positional);
109
+ if (!resolved) {
110
+ console.error(
111
+ `rnbs-rules: unknown target "${positional}".\n` +
112
+ `Expected one of: ${Object.keys(TARGETS).join(', ')} — or a path ending in .md / .mdc.\n` +
113
+ `Run "npx rnbs-rules --help" for usage.`
114
+ );
115
+ process.exitCode = 1;
116
+ return;
117
+ }
118
+
119
+ const { target, relPath } = resolved;
120
+ const outPath = path.resolve(process.cwd(), relPath);
121
+
122
+ if (fs.existsSync(outPath) && !force) {
123
+ console.error(
124
+ `rnbs-rules: ${relPath} already exists. Re-run with --force to overwrite.`
125
+ );
126
+ process.exitCode = 1;
127
+ return;
128
+ }
129
+
130
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
131
+ fs.writeFileSync(outPath, frontmatterFor(target) + rules, 'utf8');
132
+
133
+ console.log($lf(139), `rnbs-rules: wrote ${relPath}`);
134
+ if (target === 'agents') {
135
+ console.log(
136
+ 'Tip: if you already have an AGENTS.md, use a custom path instead and ' +
137
+ 'link to it, e.g. npx rnbs-rules docs/ai/bottom-sheet.md'
138
+ );
139
+ }
140
+ }
141
+
142
+ main();
143
+ function $lf(n) {
144
+ return '$lf|bin/install-rules.js:' + n + ' >';
145
+ // Automatically injected by Log Location Injector vscode extension
146
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shaquillehinds/react-native-bottom-sheet",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "A simple bottom sheet for react native that just works.",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/commonjs/index.js",
@@ -20,12 +20,17 @@
20
20
  }
21
21
  }
22
22
  },
23
+ "bin": {
24
+ "rnbs-rules": "./bin/install-rules.js"
25
+ },
23
26
  "files": [
24
27
  "lib",
25
28
  "src",
26
29
  "android",
27
30
  "ios",
28
31
  "cpp",
32
+ "rules",
33
+ "bin",
29
34
  "*.podspec",
30
35
  "react-native.config.js",
31
36
  "!ios/build",
@@ -0,0 +1,373 @@
1
+ # Agent Rules — `@shaquillehinds/react-native-bottom-sheet`
2
+
3
+ Rules for AI coding agents writing or modifying code that uses this package.
4
+ Read this before writing any bottom sheet. Follow it over general knowledge of
5
+ other bottom sheet libraries — the API here is not `@gorhom/bottom-sheet` and
6
+ patterns from that library will not work.
7
+
8
+ ---
9
+
10
+ ## 0. Non-negotiables
11
+
12
+ 1. **Always split the sheet into a wrapper component and a `*Content` component.**
13
+ State, hooks, queries and derived data live in `*Content`, never in the wrapper.
14
+ See Rule 1 — this is the single most important rule in this file.
15
+ 2. `BottomSheetPortalProvider` must wrap the app root, above the navigation container.
16
+ 3. Scrollable content inside a sheet uses `BottomSheetFlatlist` or
17
+ `BottomSheetScrollView`. Never a bare `FlatList`, `ScrollView` or `FlashList`.
18
+ 4. Do not add a `<Modal>`, `KeyboardAvoidingView`, `SafeAreaView` or
19
+ `TouchableWithoutFeedback` backdrop around the sheet. All of that is handled.
20
+ 5. Never guess prop names. If a prop is not in this file, it does not exist.
21
+
22
+ ---
23
+
24
+ ## 1. Render isolation: keep state in the content component
25
+
26
+ The sheet's children are only mounted while the sheet is open. Anything written
27
+ _inside the child component_ therefore costs nothing until the user opens it.
28
+ Anything written _in the wrapper's function body_ runs on every parent render,
29
+ open or closed.
30
+
31
+ So the wrapper is a thin shell that takes props and renders `<XContent />`.
32
+ All hooks — `useState`, `useEffect`, data fetching, store subscriptions,
33
+ context reads, expensive `useMemo`, list construction — go in `XContent`.
34
+
35
+ ### Correct
36
+
37
+ ```tsx
38
+ export default function ChooseFlashcardsModal(props: ChooseFlashcardsProps) {
39
+ const { colors, mode } = useTheme();
40
+ return (
41
+ <SmoothBottomModal
42
+ showModal={props.toggled}
43
+ setShowModal={props.setToggled}
44
+ backgroundColor={colors.background[mode]}
45
+ showContentDelay={{ timeInMilliSecs: 250, type: 'mount' }}
46
+ snapPoints={[90]}
47
+ >
48
+ <ChooseFlashcardsContent {...props} />
49
+ </SmoothBottomModal>
50
+ );
51
+ }
52
+
53
+ // Nothing in here runs until the sheet is open.
54
+ function ChooseFlashcardsContent(props: ChooseFlashcardsProps) {
55
+ const { relativeY, orientation } = useDeviceOrientation();
56
+ const t = useTranslation();
57
+ const { data } = useFlashcardSets(props.studySetId);
58
+ return <>{/* ... */}</>;
59
+ }
60
+ ```
61
+
62
+ ### Wrong
63
+
64
+ ```tsx
65
+ export default function ChooseFlashcardsModal(props: ChooseFlashcardsProps) {
66
+ // ❌ These run on every render of the parent screen, even with the sheet closed.
67
+ const { data } = useFlashcardSets(props.studySetId);
68
+ const [selected, setSelected] = useState<FlashcardSet[]>([]);
69
+ const rows = useMemo(() => buildRows(data), [data]);
70
+
71
+ return (
72
+ <SmoothBottomModal
73
+ showModal={props.toggled}
74
+ setShowModal={props.setToggled}
75
+ >
76
+ {/* ❌ Inline JSX with logic — same problem, plus it re-renders with the screen. */}
77
+ {rows.map((r) => (
78
+ <Row key={r.id} {...r} />
79
+ ))}
80
+ </SmoothBottomModal>
81
+ );
82
+ }
83
+ ```
84
+
85
+ ### Why this works
86
+
87
+ `BottomSheetModal` passes its subtree through a `ComponentMounter` keyed on
88
+ `showModal`. While closed, the subtree is unmounted, so no hook inside a child
89
+ component has run yet. Creating the element `<XContent />` is nearly free; it is
90
+ _calling_ the component that is expensive, and that only happens on mount.
91
+
92
+ Hooks in the wrapper body are in the parent's render, outside the mounter, so
93
+ they always run. That is the whole distinction.
94
+
95
+ ### Corollaries
96
+
97
+ - Do **not** hoist state out of `*Content` into the wrapper "so it survives close".
98
+ If state must survive, lift it to the screen that owns the sheet and pass it
99
+ down as props (as `ChooseFlashcards` does with `selectedFlashcards`).
100
+ - Do not read a store or context in the wrapper unless the _sheet's own props_
101
+ need it. Theme colours for `backgroundColor` are the normal exception, since
102
+ the sheet chrome needs them.
103
+ - The pattern applies to `BottomSheet` (inline) exactly as it does to `BottomSheetModal`.
104
+
105
+ ### Pair it with `showContentDelay`
106
+
107
+ For anything non-trivial, add `showContentDelay={{ type: 'mount', timeInMilliSecs: 250 }}`.
108
+ Content then mounts _after_ the open animation, so a heavy first render never
109
+ competes with the animation.
110
+
111
+ `type: 'mount'` means the sheet has no content to measure when it opens, so it
112
+ needs to be told how tall to be. Either set `snapPoints` (preferred) or give
113
+ `contentContainerStyle` a `minHeight`. Without one of those, the sheet opens
114
+ collapsed.
115
+
116
+ Use `type: 'opacity'` (the default when `type` is omitted) only when content is
117
+ cheap and you just want a fade — it still renders immediately.
118
+
119
+ ---
120
+
121
+ ## 2. Choosing a component
122
+
123
+ | Need | Use |
124
+ | -------------------------------------------------------------------- | ----------------------- |
125
+ | Standard sheet with backdrop | `BottomSheetModal` |
126
+ | Persistent / inline sheet, no backdrop, background stays interactive | `BottomSheet` |
127
+ | Scrollable list inside a sheet | `BottomSheetFlatlist` |
128
+ | Scrollable view inside a sheet | `BottomSheetScrollView` |
129
+
130
+ `BottomSheet` renders with `enableBackgroundContentPress`, so there is no backdrop
131
+ and taps pass through to whatever is behind it. Use it for filter panels, mini
132
+ players, persistent drawers. Use `BottomSheetModal` for everything else.
133
+
134
+ ---
135
+
136
+ ## 3. Controlling the sheet
137
+
138
+ There are two mutually exclusive control modes. Pick one; do not mix them.
139
+
140
+ ### 3a. State-controlled (default — use this)
141
+
142
+ ```tsx
143
+ const [showModal, setShowModal] = useState(false);
144
+
145
+ <BottomSheetModal
146
+ showModal={showModal}
147
+ setShowModal={setShowModal}
148
+ snapPoints={[85]}
149
+ >
150
+ <MyContent onDone={() => setShowModal(false)} />
151
+ </BottomSheetModal>;
152
+ ```
153
+
154
+ Close from inside the sheet with `setShowModal(false)` passed down, or with
155
+ `useBottomSheetRef()`.
156
+
157
+ ### 3b. Ref-controlled (no `showModal` / `setShowModal` at all)
158
+
159
+ For a sheet mounted once, high in the tree, opened from anywhere:
160
+
161
+ ```tsx
162
+ export default function UploadMaterialModal() {
163
+ const ref = useRef<BottomModalRef>(null);
164
+ const setUploadModalRef = useApp((state) => state.setUploadModalRef);
165
+ useEffect(() => {
166
+ setUploadModalRef(ref);
167
+ }, [ref.current]);
168
+
169
+ return (
170
+ <SmoothBottomModal
171
+ ref={ref}
172
+ disablePortal
173
+ dragArea="full"
174
+ snapPoints={[90]}
175
+ >
176
+ <UploadMaterialModalContent />
177
+ </SmoothBottomModal>
178
+ );
179
+ }
180
+
181
+ // elsewhere
182
+ uploadModalRef.current?.openModal();
183
+ ```
184
+
185
+ Omit `showModal` and `setShowModal` entirely. The ref drives the mounter directly.
186
+ Use `disablePortal` when the sheet is already mounted at root level — there is
187
+ nothing for the portal to lift it above.
188
+
189
+ ### Ref typing
190
+
191
+ ```tsx
192
+ const ref = useRef<BottomModalRef>(null); // ✅
193
+ const ref = useRef<BottomModalRefObject>(null); // ❌ that alias is the ref *prop* type
194
+ ```
195
+
196
+ `BottomModalRefObject` is `React.Ref<BottomModalRef>` — it types the `ref` prop on
197
+ the component, not the object `useRef` returns.
198
+
199
+ ### Ref API
200
+
201
+ ```tsx
202
+ ref.current?.openModal({ onOpen });
203
+ ref.current?.closeModal({
204
+ skipAnimation,
205
+ isNavigating,
206
+ duration,
207
+ easing,
208
+ onClose,
209
+ });
210
+ ref.current?.closeWithoutAnimation();
211
+ ref.current?.snapToIndex(1);
212
+ ref.current?.snapToPercentage(75); // or '75%'
213
+ ref.current?.getModalState(); // ModalState.CLOSED | OPENING | OPEN | CLOSING
214
+ ```
215
+
216
+ Behaviour worth knowing before you call these:
217
+
218
+ - On a state-controlled sheet, `openModal()` with no `onOpen` is just
219
+ `setShowModal(true)`. Passing `onOpen` routes through the mounter instead.
220
+ - On a state-controlled sheet, `closeModal()` with no `duration` and no `onClose`
221
+ is just `setShowModal(false)`.
222
+ - `closeModal({ isNavigating: true })` shortens the animation to 100ms. Use it
223
+ before `navigation.navigate` / `router.push` on Android.
224
+ - `closeModal({ skipAnimation: true })` unmounts immediately.
225
+
226
+ ### Closing from inside the subtree
227
+
228
+ ```tsx
229
+ import { useBottomSheetRef } from '@shaquillehinds/react-native-bottom-sheet';
230
+
231
+ function Footer() {
232
+ const { modalRef } = useBottomSheetRef();
233
+ return (
234
+ <Button title="Close" onPress={() => modalRef?.current?.closeModal()} />
235
+ );
236
+ }
237
+ ```
238
+
239
+ Only works inside the sheet's own subtree. `modalRef` is optional — always
240
+ optional-chain it.
241
+
242
+ ---
243
+
244
+ ## 4. Snap points
245
+
246
+ - `snapPoints` are percentages of screen height: `[25, 50, 75]`, `['50%']`, `[85]`.
247
+ - **Omitting `snapPoints` is a real mode, not an oversight.** The sheet measures
248
+ its content and opens to that height, capped at ~110% screen height. Use it for
249
+ short, content-sized sheets.
250
+ - With `snapPoints`, the sheet opens at index `0`, so order low → high the way you
251
+ want it to appear. `snapToIndex` is indexed into the array as written.
252
+ - `onSnapPointReach(index)` only fires when `snapPoints` is set.
253
+ - Keep the array referentially stable — module constant or `useMemo`.
254
+ - On orientation change the sheet recomputes snap points and re-snaps to index 0.
255
+
256
+ ---
257
+
258
+ ## 5. `keepMounted` and `bottomOffset`
259
+
260
+ `keepMounted` changes what a downward drag does: instead of dismissing, the sheet
261
+ snaps to its lowest snap point.
262
+
263
+ **`keepMounted` requires `snapPoints`.** Without them it is ignored entirely.
264
+
265
+ Pair with `bottomOffset` to leave a peek visible:
266
+
267
+ ```tsx
268
+ <BottomSheet keepMounted snapPoints={[10, 70]} bottomOffset={100} ... />
269
+ ```
270
+
271
+ `keepMounted` does not stop `showModal={false}` from unmounting the sheet. It only
272
+ governs the drag gesture.
273
+
274
+ ---
275
+
276
+ ## 6. Scrolling
277
+
278
+ ```tsx
279
+ <BottomSheetModal snapPoints={[50, 90]} showModal={show} setShowModal={setShow}>
280
+ <BottomSheetFlatlist data={items} renderItem={renderItem} keyExtractor={k} />
281
+ </BottomSheetModal>
282
+ ```
283
+
284
+ - These components hand off gesture state to the sheet so the sheet drags when
285
+ the list is at its scroll boundary and scrolls otherwise. A bare `FlatList`
286
+ breaks that and the sheet will fight the list.
287
+ - They accept the full underlying props (`FlatListPropsWithLayout` /
288
+ `AnimatedScrollViewProps`), minus a reworked `onScroll` that also accepts a
289
+ Reanimated scroll event.
290
+ - Custom ref: use `refFlatlist` / `refScrollView`, not `ref`.
291
+ - `inverted` lists are supported and reported to the sheet.
292
+ - Outside a sheet these degrade gracefully to plain animated list/scrollview.
293
+ - Do not set `bounces` — it is forced to `false` inside a sheet.
294
+ - Avoid `dragArea="full"` with a scrollable child; the two gestures compete.
295
+ Leave `dragArea` at its default `"bumper"`.
296
+
297
+ ---
298
+
299
+ ## 7. Keyboard
300
+
301
+ - `avoidKeyboard` — pads content whenever the keyboard opens.
302
+ - `inputsForKeyboardToAvoid={[ref1, ref2]}` — only avoid for those specific inputs.
303
+ Prefer this when a sheet has inputs that should not shift the layout.
304
+ - Dragging is disabled while the keyboard is visible. Override with
305
+ `allowDragWhileKeyboardVisible` only if you have tested it.
306
+ - If opening the sheet from a focused input, dismiss the keyboard in the setter:
307
+
308
+ ```tsx
309
+ setShowModal={(value: boolean) => {
310
+ Keyboard.dismiss();
311
+ props.setShowModal(value);
312
+ }}
313
+ ```
314
+
315
+ Do not wrap sheet content in `KeyboardAvoidingView`.
316
+
317
+ ---
318
+
319
+ ## 8. Portal
320
+
321
+ - `BottomSheetPortalProvider` goes above `NavigationContainer`, not inside it.
322
+ - Sheets use the portal automatically. Do not add `useBottomSheetPortalComponent`
323
+ around a sheet — it already does this internally.
324
+ - `disablePortal` — render in place. Correct when the sheet is already at root
325
+ level, or when you deliberately want it scoped to a subtree.
326
+ - `CustomPortalContext` — scoped portals. Both the provider and the sheet must be
327
+ given the same context object.
328
+ - `useBottomSheetPortal` / `useBottomSheetPortalComponent` are for non-sheet
329
+ overlays (toasts, loaders). Do not reach for them to solve z-index problems with
330
+ a sheet; fix the provider placement instead.
331
+
332
+ ---
333
+
334
+ ## 9. Prop reference
335
+
336
+ Common to `BottomSheet` and `BottomSheetModal`:
337
+
338
+ `showModal` · `setShowModal` · `snapPoints` · `dragArea` (`'full' | 'bumper' | 'none'`,
339
+ default `'bumper'`) · `keepMounted` · `hideBumper` · `avoidKeyboard` ·
340
+ `allowDragWhileKeyboardVisible` · `inputsForKeyboardToAvoid` · `bottomOffset` ·
341
+ `showContentDelay` · `style` · `contentContainerStyle` · `bumperStyle` ·
342
+ `bumperContainerStyle` · `backgroundColor` · `BumperComponent` · `onModalShow` ·
343
+ `onModalClose` · `onSnapPointReach` · `disablePortal` · `CustomPortalContext`
344
+
345
+ `BottomSheetModal` adds:
346
+
347
+ `BackdropComponent` · `onBackDropPress` · `disableCloseOnBackdropPress` ·
348
+ `useNativeModal` · `enableBackgroundContentPress` · `disableAndroidBackButton`
349
+
350
+ Style targets, since they are easy to confuse:
351
+
352
+ - `style` → the sheet surface itself
353
+ - `contentContainerStyle` → the container around your children
354
+ - `backgroundColor` → sheet **and** bumper container; prefer this over setting
355
+ `backgroundColor` in `style`, which leaves the bumper mismatched
356
+ - `bumperStyle` → the grab handle; `bumperContainerStyle` → the area around it
357
+
358
+ ---
359
+
360
+ ## 10. Review checklist
361
+
362
+ Before finishing any change involving this package:
363
+
364
+ - [ ] Wrapper holds no state; all hooks are in a `*Content` component
365
+ - [ ] `showContentDelay` set for non-trivial content, with `snapPoints` or `minHeight`
366
+ - [ ] `snapPoints` is stable (module constant or `useMemo`), ordered low → high
367
+ - [ ] Scrollables are `BottomSheetFlatlist` / `BottomSheetScrollView`
368
+ - [ ] `keepMounted` only used together with `snapPoints`
369
+ - [ ] Ref typed `useRef<BottomModalRef>(null)`
370
+ - [ ] Only one control mode — state or ref, not both
371
+ - [ ] No `Modal`, `KeyboardAvoidingView` or hand-rolled backdrop wrapping the sheet
372
+ - [ ] `closeModal({ isNavigating: true })` before navigating away
373
+ - [ ] Portal provider above the navigation container