@peersahab/side-island 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Peersahab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+
package/README.md ADDED
@@ -0,0 +1,403 @@
1
+ # @peersahab/side-island
2
+
3
+ A Skia-powered side island overlay for React Native, with an internal virtualized list (FlatList) and an optional Provider + hooks control layer. Perfect for quick access menus, color pickers, and other side-mounted UI components.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i @peersahab/side-island
9
+ ```
10
+
11
+ ### Peer dependencies (required)
12
+
13
+ This library expects these to be installed in your app:
14
+
15
+ ```bash
16
+ npm i @shopify/react-native-skia react-native-reanimated
17
+ ```
18
+
19
+ ## Appearance
20
+
21
+ The `SideIsland` component can be positioned on either side of the screen and displays a smooth, wave-shaped overlay with a virtualized list of items.
22
+
23
+ ### Right Position
24
+
25
+ ![Right Island](screenshots/right-island.png)
26
+
27
+ ### Left Position
28
+
29
+ ![Left Island](screenshots/left-island.png)
30
+
31
+ ## Quick Start
32
+
33
+ ### Basic Usage
34
+
35
+ ```tsx
36
+ import React from "react";
37
+ import { View } from "react-native";
38
+ import { SideIsland } from "@peersahab/side-island";
39
+
40
+ export function Example() {
41
+ const items = Array.from({ length: 40 }).map((_, i) => ({ id: String(i) }));
42
+
43
+ return (
44
+ <View style={{ flex: 1 }}>
45
+ <SideIsland
46
+ items={items}
47
+ keyExtractor={(item) => item.id}
48
+ renderItem={({ item }) => (
49
+ <View style={{ width: 32, height: 32, borderRadius: 16, backgroundColor: "#00000022" }} />
50
+ )}
51
+ />
52
+ </View>
53
+ );
54
+ }
55
+ ```
56
+
57
+ ### With Haptics
58
+
59
+ ```tsx
60
+ import React from "react";
61
+ import * as Haptics from "expo-haptics";
62
+ import { SideIsland } from "@peersahab/side-island";
63
+
64
+ export function Example() {
65
+ return (
66
+ <SideIsland
67
+ items={items}
68
+ renderItem={({ item }) => <YourItem item={item} />}
69
+ haptics={{
70
+ onOpen: () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light),
71
+ onClose: () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light),
72
+ onFocusChange: () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Rigid),
73
+ }}
74
+ />
75
+ );
76
+ }
77
+ ```
78
+
79
+ ### With Backdrop and Focused Item Detail
80
+
81
+ ```tsx
82
+ import React from "react";
83
+ import { BlurView } from "expo-blur";
84
+ import { useWindowDimensions } from "react-native";
85
+ import { SideIsland } from "@peersahab/side-island";
86
+
87
+ export function Example() {
88
+ const { width, height } = useWindowDimensions();
89
+ const [expanded, setExpanded] = useState(false);
90
+
91
+ return (
92
+ <SideIsland
93
+ expanded={expanded}
94
+ onToggleExpanded={setExpanded}
95
+ items={people}
96
+ renderItem={({ item }) => <AvatarItem person={item} />}
97
+ backdropComponent={
98
+ <BlurView intensity={20} style={{ width, height }} tint="dark" />
99
+ }
100
+ renderFocusedItemDetail={({ item }) => (
101
+ <View>
102
+ <Text>{item.name}</Text>
103
+ <Text>{item.email}</Text>
104
+ </View>
105
+ )}
106
+ />
107
+ );
108
+ }
109
+ ```
110
+
111
+ ## Provider + Hook Pattern
112
+
113
+ ### Setup Provider
114
+
115
+ Wrap your app once with the provider:
116
+
117
+ ```tsx
118
+ import React from "react";
119
+ import { SideIslandProvider } from "@peersahab/side-island";
120
+
121
+ export function AppRoot() {
122
+ return (
123
+ <SideIslandProvider
124
+ defaultExpanded={false}
125
+ config={{
126
+ position: "right",
127
+ width: 40,
128
+ height: 300,
129
+ }}
130
+ >
131
+ {/* your navigation tree */}
132
+ </SideIslandProvider>
133
+ );
134
+ }
135
+ ```
136
+
137
+ ### Control from Anywhere
138
+
139
+ Use the hook to control the island from any component:
140
+
141
+ ```tsx
142
+ import React from "react";
143
+ import { Button } from "react-native";
144
+ import { useSideIsland } from "@peersahab/side-island";
145
+
146
+ export function ToggleIslandButton() {
147
+ const island = useSideIsland();
148
+
149
+ return (
150
+ <Button
151
+ title={island.expanded ? "Close" : "Open"}
152
+ onPress={island.toggle}
153
+ />
154
+ );
155
+ }
156
+ ```
157
+
158
+ ### Render Island (uses Provider state)
159
+
160
+ When using the provider, the island automatically uses the provider's state:
161
+
162
+ ```tsx
163
+ import React from "react";
164
+ import { SideIsland } from "@peersahab/side-island";
165
+
166
+ export function IslandOverlay() {
167
+ return (
168
+ <SideIsland
169
+ items={items}
170
+ renderItem={({ item }) => <YourItem item={item} />}
171
+ // No need to pass expanded/onToggleExpanded - uses provider state
172
+ />
173
+ );
174
+ }
175
+ ```
176
+
177
+ ## API Reference
178
+
179
+ ### `SideIsland<ItemT>`
180
+
181
+ The main component that renders the side island overlay.
182
+
183
+ #### Required Props
184
+
185
+ | Prop | Type | Description |
186
+ |------|------|-------------|
187
+ | `items` | `readonly ItemT[]` | Array of items to display in the island |
188
+ | `renderItem` | `(info: { item: ItemT; index: number }) => React.ReactElement \| null` | Function to render each item |
189
+
190
+ #### Optional Props
191
+
192
+ ##### List Configuration
193
+
194
+ | Prop | Type | Default | Description |
195
+ |------|------|---------|-------------|
196
+ | `keyExtractor` | `(item: ItemT, index: number) => string` | `(_, index) => String(index)` | Function to extract unique keys for items |
197
+ | `listProps` | `Omit<FlatListProps<ItemT>, "data" \| "renderItem" \| "keyExtractor">` | - | Additional props to pass to the internal FlatList |
198
+ | `onFocusedItemChange` | `(info: { item: ItemT; index: number } \| null) => void` | - | Called when the focused item changes (item closest to center) |
199
+
200
+ ##### Position & Layout
201
+
202
+ | Prop | Type | Default | Description |
203
+ |------|------|---------|-------------|
204
+ | `position` | `"left" \| "right"` | `"right"` | Which side of the screen the island is pinned to |
205
+ | `width` | `number` | `40` | Width of the island in pixels |
206
+ | `height` | `number` | `250` | Height of the island in pixels |
207
+ | `topOffset` | `number` | `0` | Vertical offset from center in pixels (positive = down, negative = up) |
208
+ | `style` | `ViewStyle` | - | Additional styles for the island container |
209
+
210
+ ##### Wave Shape (Advanced)
211
+
212
+ | Prop | Type | Default | Description |
213
+ |------|------|---------|-------------|
214
+ | `waveAmplitude` | `number` | `18` | Amplitude of the wave bulge (controls how much it extends) |
215
+ | `waveY1` | `number` | `0.1` | Top wave peak position (0-1, relative to height) |
216
+ | `waveY2` | `number` | `0.9` | Bottom wave peak position (0-1, relative to height) |
217
+
218
+ ##### Appearance
219
+
220
+ | Prop | Type | Default | Description |
221
+ |------|------|---------|-------------|
222
+ | `backgroundColor` | `string` | `"#000000"` | Background color of the island |
223
+ | `handleWidth` | `number` | `16` | Width of the touchable handle area (0 to disable) |
224
+
225
+ ##### State Management
226
+
227
+ | Prop | Type | Default | Description |
228
+ |------|------|---------|-------------|
229
+ | `expanded` | `boolean` | - | Controlled expanded state (if provided, component is controlled) |
230
+ | `onToggleExpanded` | `(next: boolean) => void` | - | Callback when expanded state should change (required if `expanded` is provided) |
231
+ | `defaultExpanded` | `boolean` | `false` | Initial expanded state (only used if uncontrolled) |
232
+
233
+ ##### Interaction
234
+
235
+ | Prop | Type | Default | Description |
236
+ |------|------|---------|-------------|
237
+ | `onPress` | `() => void` | - | Fired when the handle area is pressed (island still toggles automatically) |
238
+ | `haptics` | `SideIslandHaptics` | - | Haptics adapter for feedback (see Haptics section below) |
239
+
240
+ ##### Advanced Features
241
+
242
+ | Prop | Type | Default | Description |
243
+ |------|------|---------|-------------|
244
+ | `backdropComponent` | `React.ReactElement` | - | Component to render as backdrop (fades in when expanded) |
245
+ | `renderFocusedItemDetail` | `(info: { item: ItemT; index: number; expanded: boolean; setExpanded: (next: boolean) => void }) => React.ReactElement \| null` | - | Component to render details of the focused item (displayed opposite the island) |
246
+ | `focusedItemDetailGap` | `number` | `16` | Horizontal gap between focused item detail and island |
247
+
248
+ ### `SideIslandProvider`
249
+
250
+ Provider component for managing island state globally.
251
+
252
+ #### Props
253
+
254
+ | Prop | Type | Default | Description |
255
+ |------|------|---------|-------------|
256
+ | `children` | `React.ReactNode` | - | Child components |
257
+ | `defaultExpanded` | `boolean` | `false` | Initial expanded state |
258
+ | `onExpandedChange` | `(next: boolean) => void` | - | Callback when expanded state changes |
259
+ | `config` | `SideIslandConfig` | - | Default configuration for all islands (see `SideIslandConfig` below) |
260
+ | `value` | `{ expanded: boolean; setExpanded: (next: boolean) => void; config?: SideIslandConfig }` | - | External state control (for advanced use cases) |
261
+
262
+ ### `useSideIsland()`
263
+
264
+ Hook to access and control the island state from the provider.
265
+
266
+ #### Returns
267
+
268
+ ```typescript
269
+ {
270
+ expanded: boolean;
271
+ setExpanded: (next: boolean) => void;
272
+ open: () => void;
273
+ close: () => void;
274
+ toggle: () => void;
275
+ config: SideIslandConfig;
276
+ }
277
+ ```
278
+
279
+ ### `SideIslandConfig`
280
+
281
+ Configuration object for default island settings (used in Provider or individual islands).
282
+
283
+ | Property | Type | Default | Description |
284
+ |----------|------|---------|-------------|
285
+ | `position` | `"left" \| "right"` | `"right"` | Default position |
286
+ | `width` | `number` | `40` | Default width |
287
+ | `height` | `number` | `250` | Default height |
288
+ | `waveAmplitude` | `number` | `18` | Default wave amplitude |
289
+ | `waveY1` | `number` | `0.1` | Default top wave position |
290
+ | `waveY2` | `number` | `0.9` | Default bottom wave position |
291
+ | `backgroundColor` | `string` | `"#000000"` | Default background color |
292
+ | `handleWidth` | `number` | `16` | Default handle width |
293
+ | `topOffset` | `number` | `0` | Default top offset |
294
+ | `haptics` | `SideIslandHaptics` | - | Default haptics adapter |
295
+
296
+ ### `SideIslandHaptics`
297
+
298
+ Haptics adapter interface for providing haptic feedback.
299
+
300
+ | Property | Type | Description |
301
+ |----------|------|-------------|
302
+ | `onOpen` | `() => void \| Promise<void>` | Called when island opens |
303
+ | `onClose` | `() => void \| Promise<void>` | Called when island closes |
304
+ | `onFocusChange` | `(info: { index: number } \| null) => void \| Promise<void>` | Called when focused item changes |
305
+
306
+ **Example with expo-haptics:**
307
+
308
+ ```tsx
309
+ import * as Haptics from "expo-haptics";
310
+
311
+ <SideIsland
312
+ haptics={{
313
+ onOpen: () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light),
314
+ onClose: () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light),
315
+ onFocusChange: () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Rigid),
316
+ }}
317
+ // ... other props
318
+ />
319
+ ```
320
+
321
+ ## Running the Example App
322
+
323
+ The repository includes an example Expo app demonstrating various use cases.
324
+
325
+ ### Prerequisites
326
+
327
+ - Node.js (v18 or later)
328
+ - npm or yarn
329
+ - Expo CLI (optional, but recommended): `npm install -g expo-cli`
330
+
331
+ ### Setup
332
+
333
+ 1. **Install dependencies for the main package:**
334
+ ```bash
335
+ npm install
336
+ ```
337
+
338
+ 2. **Build the package:**
339
+ ```bash
340
+ npm run build
341
+ ```
342
+
343
+ 3. **Navigate to the example app:**
344
+ ```bash
345
+ cd apps/example
346
+ ```
347
+
348
+ 4. **Install example app dependencies:**
349
+ ```bash
350
+ npm install
351
+ ```
352
+
353
+ ### Running
354
+
355
+ **Start the Expo development server:**
356
+ ```bash
357
+ npm start
358
+ ```
359
+
360
+ This will open the Expo DevTools. From there you can:
361
+
362
+ - Press `i` to open iOS simulator (requires Xcode on macOS)
363
+ - Press `a` to open Android emulator (requires Android Studio)
364
+ - Scan the QR code with Expo Go app on your physical device
365
+ - Press `w` to open in web browser
366
+
367
+ **Or use the specific platform commands:**
368
+ ```bash
369
+ npm run ios # iOS simulator
370
+ npm run android # Android emulator
371
+ npm run web # Web browser
372
+ ```
373
+
374
+ ### Example App Features
375
+
376
+ The example app demonstrates:
377
+ - People picker with avatar items
378
+ - Color picker with label colors
379
+ - Focused item detail views
380
+ - Backdrop blur effects
381
+ - Haptic feedback integration
382
+ - Multiple islands (left and right positioned)
383
+
384
+ ## TypeScript
385
+
386
+ The library is written in TypeScript and includes full type definitions. All exports are properly typed:
387
+
388
+ ```typescript
389
+ import {
390
+ SideIsland,
391
+ SideIslandProvider,
392
+ useSideIsland,
393
+ type SideIslandProps,
394
+ type SideIslandConfig,
395
+ type SideIslandController,
396
+ type SideIslandPosition,
397
+ type SideIslandHaptics,
398
+ } from "@peersahab/side-island";
399
+ ```
400
+
401
+ ## License
402
+
403
+ MIT
@@ -0,0 +1,146 @@
1
+ import React from 'react';
2
+ import { FlatListProps, ViewStyle } from 'react-native';
3
+
4
+ type TeamMember = {
5
+ id: string;
6
+ name: string;
7
+ avatar?: string;
8
+ role: string;
9
+ status?: "active" | "inactive";
10
+ };
11
+ type SideIslandHaptics = {
12
+ /**
13
+ * Called when the island opens (expanded becomes true).
14
+ * Implement this using your own haptics library (e.g. expo-haptics).
15
+ */
16
+ onOpen?: () => void | Promise<void>;
17
+ /**
18
+ * Called when the island closes (expanded becomes false).
19
+ * Implement this using your own haptics library (e.g. expo-haptics).
20
+ */
21
+ onClose?: () => void | Promise<void>;
22
+ /**
23
+ * Called when the focused item changes while scrolling.
24
+ * Use this to trigger a "rigid" (or other) haptic in your app.
25
+ */
26
+ onFocusChange?: (info: {
27
+ index: number;
28
+ } | null) => void | Promise<void>;
29
+ };
30
+ type SideIslandPosition = "left" | "right";
31
+ type SideIslandConfig = {
32
+ /**
33
+ * Which side of the screen the island is pinned to.
34
+ * Default: "right"
35
+ */
36
+ position?: SideIslandPosition;
37
+ width?: number;
38
+ height?: number;
39
+ waveAmplitude?: number;
40
+ waveY1?: number;
41
+ waveY2?: number;
42
+ backgroundColor?: string;
43
+ handleWidth?: number;
44
+ topOffset?: number;
45
+ /**
46
+ * Optional haptics adapter. If provided, it will be used to trigger haptic feedback
47
+ * on island open/close without adding a hard dependency to any haptics library.
48
+ */
49
+ haptics?: SideIslandHaptics;
50
+ };
51
+ type SideIslandController = {
52
+ expanded: boolean;
53
+ setExpanded: (next: boolean) => void;
54
+ open: () => void;
55
+ close: () => void;
56
+ toggle: () => void;
57
+ config: SideIslandConfig;
58
+ };
59
+ type SideIslandProviderProps = {
60
+ children: React.ReactNode;
61
+ defaultExpanded?: boolean;
62
+ onExpandedChange?: (next: boolean) => void;
63
+ config?: SideIslandConfig;
64
+ value?: {
65
+ expanded: boolean;
66
+ setExpanded: (next: boolean) => void;
67
+ config?: SideIslandConfig;
68
+ };
69
+ };
70
+ type SideIslandProps<ItemT> = {
71
+ items: readonly ItemT[];
72
+ renderItem: (info: {
73
+ item: ItemT;
74
+ index: number;
75
+ }) => React.ReactElement | null;
76
+ keyExtractor?: (item: ItemT, index: number) => string;
77
+ listProps?: Omit<FlatListProps<ItemT>, "data" | "renderItem" | "keyExtractor">;
78
+ /**
79
+ * Called whenever the "focused" item changes as the user scrolls.
80
+ * Focus is determined by the item closest to the vertical center of the island.
81
+ * On first open, the island scrolls to focus the first item (index 0) centered.
82
+ * On subsequent opens, the island scrolls back to the last focused item.
83
+ */
84
+ onFocusedItemChange?: (info: {
85
+ item: ItemT;
86
+ index: number;
87
+ } | null) => void;
88
+ /**
89
+ * Which side of the screen the island is pinned to.
90
+ * Default: "right"
91
+ */
92
+ position?: SideIslandPosition;
93
+ width?: number;
94
+ height?: number;
95
+ waveAmplitude?: number;
96
+ waveY1?: number;
97
+ waveY2?: number;
98
+ backgroundColor?: string;
99
+ handleWidth?: number;
100
+ topOffset?: number;
101
+ /**
102
+ * Optional haptics adapter. If provided, it will be used to trigger haptic feedback
103
+ * on island open/close without adding a hard dependency to any haptics library.
104
+ */
105
+ haptics?: SideIslandHaptics;
106
+ style?: ViewStyle;
107
+ expanded?: boolean;
108
+ onToggleExpanded?: (next: boolean) => void;
109
+ defaultExpanded?: boolean;
110
+ /**
111
+ * Fired when the handle area is pressed.
112
+ * The island will still toggle expansion via controlled/provider/internal state.
113
+ */
114
+ onPress?: () => void;
115
+ /**
116
+ * Optional backdrop component that will fade into view when the island expands.
117
+ * Should cover the full screen and be positioned behind the island.
118
+ */
119
+ backdropComponent?: React.ReactElement;
120
+ /**
121
+ * Optional component to render details of the currently focused item.
122
+ * Displayed on top of the backdrop, opposite of the island:
123
+ * - position="right" => detail is to the left of the island
124
+ * - position="left" => detail is to the right of the island
125
+ * Receives the focused item info and can interact with the island.
126
+ */
127
+ renderFocusedItemDetail?: (info: {
128
+ item: ItemT;
129
+ index: number;
130
+ expanded: boolean;
131
+ setExpanded: (next: boolean) => void;
132
+ }) => React.ReactElement | null;
133
+ /**
134
+ * Horizontal gap between the focused item detail component and the island.
135
+ * Default: 16
136
+ */
137
+ focusedItemDetailGap?: number;
138
+ };
139
+
140
+ declare function SideIsland<ItemT>({ items, renderItem, keyExtractor, listProps, onFocusedItemChange, position, width, height, waveAmplitude, waveY1, waveY2, backgroundColor, handleWidth, topOffset, haptics, style, expanded, onToggleExpanded, defaultExpanded, onPress, backdropComponent, renderFocusedItemDetail, focusedItemDetailGap, }: SideIslandProps<ItemT>): React.JSX.Element;
141
+
142
+ declare function SideIslandProvider({ children, defaultExpanded, onExpandedChange, config, value, }: SideIslandProviderProps): React.JSX.Element;
143
+
144
+ declare function useSideIsland(): SideIslandController;
145
+
146
+ export { SideIsland, type SideIslandConfig, type SideIslandController, type SideIslandPosition, type SideIslandProps, SideIslandProvider, type SideIslandProviderProps, type TeamMember, useSideIsland };