@indra-inter/neobrutalism-components 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/README.md +1330 -0
- package/dist/module/assets/avatar-profile.png +0 -0
- package/dist/module/components/accordion/accordion.js +203 -0
- package/dist/module/components/accordion/accordion.js.map +1 -0
- package/dist/module/components/app-bar/app-bar.js +140 -0
- package/dist/module/components/app-bar/app-bar.js.map +1 -0
- package/dist/module/components/avatar/avatar.js +184 -0
- package/dist/module/components/avatar/avatar.js.map +1 -0
- package/dist/module/components/badge/badge.js +133 -0
- package/dist/module/components/badge/badge.js.map +1 -0
- package/dist/module/components/bottom-navigation/bottom-navigation.js +166 -0
- package/dist/module/components/bottom-navigation/bottom-navigation.js.map +1 -0
- package/dist/module/components/bottom-sheet/bottom-sheet.js +285 -0
- package/dist/module/components/bottom-sheet/bottom-sheet.js.map +1 -0
- package/dist/module/components/button/button.js +253 -0
- package/dist/module/components/button/button.js.map +1 -0
- package/dist/module/components/calendar/calendar.js +458 -0
- package/dist/module/components/calendar/calendar.js.map +1 -0
- package/dist/module/components/call-item/call-item.js +128 -0
- package/dist/module/components/call-item/call-item.js.map +1 -0
- package/dist/module/components/card/card.js +189 -0
- package/dist/module/components/card/card.js.map +1 -0
- package/dist/module/components/chat-list-item/chat-list-item.js +212 -0
- package/dist/module/components/chat-list-item/chat-list-item.js.map +1 -0
- package/dist/module/components/checkbox/checkbox.js +141 -0
- package/dist/module/components/checkbox/checkbox.js.map +1 -0
- package/dist/module/components/dialog/dialog.js +342 -0
- package/dist/module/components/dialog/dialog.js.map +1 -0
- package/dist/module/components/divider/divider.js +41 -0
- package/dist/module/components/divider/divider.js.map +1 -0
- package/dist/module/components/dropdown/dropdown-menu.js +214 -0
- package/dist/module/components/dropdown/dropdown-menu.js.map +1 -0
- package/dist/module/components/dropdown/dropdown.js +616 -0
- package/dist/module/components/dropdown/dropdown.js.map +1 -0
- package/dist/module/components/dropdown/types.js +4 -0
- package/dist/module/components/dropdown/types.js.map +1 -0
- package/dist/module/components/icon-button/icon-button.js +140 -0
- package/dist/module/components/icon-button/icon-button.js.map +1 -0
- package/dist/module/components/input/currency-input.js +60 -0
- package/dist/module/components/input/currency-input.js.map +1 -0
- package/dist/module/components/input/input.js +281 -0
- package/dist/module/components/input/input.js.map +1 -0
- package/dist/module/components/input/otp-input.js +196 -0
- package/dist/module/components/input/otp-input.js.map +1 -0
- package/dist/module/components/input/password-input.js +74 -0
- package/dist/module/components/input/password-input.js.map +1 -0
- package/dist/module/components/input/phone-input.js +68 -0
- package/dist/module/components/input/phone-input.js.map +1 -0
- package/dist/module/components/input/url-input.js +43 -0
- package/dist/module/components/input/url-input.js.map +1 -0
- package/dist/module/components/list-item/list-item.js +148 -0
- package/dist/module/components/list-item/list-item.js.map +1 -0
- package/dist/module/components/pin-code-field/pin-code-field.js +164 -0
- package/dist/module/components/pin-code-field/pin-code-field.js.map +1 -0
- package/dist/module/components/progress/progress.js +261 -0
- package/dist/module/components/progress/progress.js.map +1 -0
- package/dist/module/components/radio/radio.js +135 -0
- package/dist/module/components/radio/radio.js.map +1 -0
- package/dist/module/components/skeleton/skeleton.js +328 -0
- package/dist/module/components/skeleton/skeleton.js.map +1 -0
- package/dist/module/components/switch/switch.js +170 -0
- package/dist/module/components/switch/switch.js.map +1 -0
- package/dist/module/components/tabs/tabs.js +197 -0
- package/dist/module/components/tabs/tabs.js.map +1 -0
- package/dist/module/components/toast/toast-provider.js +117 -0
- package/dist/module/components/toast/toast-provider.js.map +1 -0
- package/dist/module/components/toast/toast.js +154 -0
- package/dist/module/components/toast/toast.js.map +1 -0
- package/dist/module/index.js +38 -0
- package/dist/module/index.js.map +1 -0
- package/dist/module/package.json +1 -0
- package/dist/module/theme/design-tokens.generated.js +379 -0
- package/dist/module/theme/design-tokens.generated.js.map +1 -0
- package/dist/module/theme/theme-provider.js +20 -0
- package/dist/module/theme/theme-provider.js.map +1 -0
- package/dist/module/theme/theme.js +56 -0
- package/dist/module/theme/theme.js.map +1 -0
- package/dist/typescript/components/accordion/accordion.d.ts +40 -0
- package/dist/typescript/components/accordion/accordion.d.ts.map +1 -0
- package/dist/typescript/components/app-bar/app-bar.d.ts +34 -0
- package/dist/typescript/components/app-bar/app-bar.d.ts.map +1 -0
- package/dist/typescript/components/avatar/avatar.d.ts +17 -0
- package/dist/typescript/components/avatar/avatar.d.ts.map +1 -0
- package/dist/typescript/components/badge/badge.d.ts +20 -0
- package/dist/typescript/components/badge/badge.d.ts.map +1 -0
- package/dist/typescript/components/bottom-navigation/bottom-navigation.d.ts +28 -0
- package/dist/typescript/components/bottom-navigation/bottom-navigation.d.ts.map +1 -0
- package/dist/typescript/components/bottom-sheet/bottom-sheet.d.ts +38 -0
- package/dist/typescript/components/bottom-sheet/bottom-sheet.d.ts.map +1 -0
- package/dist/typescript/components/button/button.d.ts +21 -0
- package/dist/typescript/components/button/button.d.ts.map +1 -0
- package/dist/typescript/components/calendar/calendar.d.ts +41 -0
- package/dist/typescript/components/calendar/calendar.d.ts.map +1 -0
- package/dist/typescript/components/call-item/call-item.d.ts +21 -0
- package/dist/typescript/components/call-item/call-item.d.ts.map +1 -0
- package/dist/typescript/components/card/card.d.ts +37 -0
- package/dist/typescript/components/card/card.d.ts.map +1 -0
- package/dist/typescript/components/chat-list-item/chat-list-item.d.ts +37 -0
- package/dist/typescript/components/chat-list-item/chat-list-item.d.ts.map +1 -0
- package/dist/typescript/components/checkbox/checkbox.d.ts +15 -0
- package/dist/typescript/components/checkbox/checkbox.d.ts.map +1 -0
- package/dist/typescript/components/dialog/dialog.d.ts +34 -0
- package/dist/typescript/components/dialog/dialog.d.ts.map +1 -0
- package/dist/typescript/components/divider/divider.d.ts +10 -0
- package/dist/typescript/components/divider/divider.d.ts.map +1 -0
- package/dist/typescript/components/dropdown/dropdown-menu.d.ts +27 -0
- package/dist/typescript/components/dropdown/dropdown-menu.d.ts.map +1 -0
- package/dist/typescript/components/dropdown/dropdown.d.ts +4 -0
- package/dist/typescript/components/dropdown/dropdown.d.ts.map +1 -0
- package/dist/typescript/components/dropdown/types.d.ts +52 -0
- package/dist/typescript/components/dropdown/types.d.ts.map +1 -0
- package/dist/typescript/components/icon-button/icon-button.d.ts +20 -0
- package/dist/typescript/components/icon-button/icon-button.d.ts.map +1 -0
- package/dist/typescript/components/input/currency-input.d.ts +10 -0
- package/dist/typescript/components/input/currency-input.d.ts.map +1 -0
- package/dist/typescript/components/input/input.d.ts +32 -0
- package/dist/typescript/components/input/input.d.ts.map +1 -0
- package/dist/typescript/components/input/otp-input.d.ts +23 -0
- package/dist/typescript/components/input/otp-input.d.ts.map +1 -0
- package/dist/typescript/components/input/password-input.d.ts +14 -0
- package/dist/typescript/components/input/password-input.d.ts.map +1 -0
- package/dist/typescript/components/input/phone-input.d.ts +13 -0
- package/dist/typescript/components/input/phone-input.d.ts.map +1 -0
- package/dist/typescript/components/input/url-input.d.ts +10 -0
- package/dist/typescript/components/input/url-input.d.ts.map +1 -0
- package/dist/typescript/components/list-item/list-item.d.ts +30 -0
- package/dist/typescript/components/list-item/list-item.d.ts.map +1 -0
- package/dist/typescript/components/pin-code-field/pin-code-field.d.ts +25 -0
- package/dist/typescript/components/pin-code-field/pin-code-field.d.ts.map +1 -0
- package/dist/typescript/components/progress/progress.d.ts +18 -0
- package/dist/typescript/components/progress/progress.d.ts.map +1 -0
- package/dist/typescript/components/radio/radio.d.ts +12 -0
- package/dist/typescript/components/radio/radio.d.ts.map +1 -0
- package/dist/typescript/components/skeleton/skeleton.d.ts +16 -0
- package/dist/typescript/components/skeleton/skeleton.d.ts.map +1 -0
- package/dist/typescript/components/switch/switch.d.ts +12 -0
- package/dist/typescript/components/switch/switch.d.ts.map +1 -0
- package/dist/typescript/components/tabs/tabs.d.ts +37 -0
- package/dist/typescript/components/tabs/tabs.d.ts.map +1 -0
- package/dist/typescript/components/toast/toast-provider.d.ts +22 -0
- package/dist/typescript/components/toast/toast-provider.d.ts.map +1 -0
- package/dist/typescript/components/toast/toast.d.ts +15 -0
- package/dist/typescript/components/toast/toast.d.ts.map +1 -0
- package/dist/typescript/index.d.ts +36 -0
- package/dist/typescript/index.d.ts.map +1 -0
- package/dist/typescript/package.json +1 -0
- package/dist/typescript/theme/design-tokens.generated.d.ts +375 -0
- package/dist/typescript/theme/design-tokens.generated.d.ts.map +1 -0
- package/dist/typescript/theme/theme-provider.d.ts +8 -0
- package/dist/typescript/theme/theme-provider.d.ts.map +1 -0
- package/dist/typescript/theme/theme.d.ts +52 -0
- package/dist/typescript/theme/theme.d.ts.map +1 -0
- package/package.json +88 -0
- package/tokens.css +132 -0
package/README.md
ADDED
|
@@ -0,0 +1,1330 @@
|
|
|
1
|
+
# Neobrutalism Components
|
|
2
|
+
|
|
3
|
+
A token-driven neobrutalism component library for React Native iOS and Android.
|
|
4
|
+
It provides reusable foundation, form, selection, feedback, and overlay
|
|
5
|
+
components with light and dark themes.
|
|
6
|
+
|
|
7
|
+
> This package is currently used as a local package. The npm command below is
|
|
8
|
+
> the intended installation flow after the package has been published.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Install the native peer dependencies with Expo:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx expo install react-native-safe-area-context react-native-screens react-native-svg
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
During local development, add the package from its sibling directory:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm add ../neobrutalism-components-rn
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
After publishing to npm:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm add neobrutalism-components
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## NativeWind and React Native styles
|
|
31
|
+
|
|
32
|
+
NativeWind v4 is a required peer dependency. Configure it in the consuming
|
|
33
|
+
application, then import components from the same package entry point. Neo's
|
|
34
|
+
token-driven `StyleSheet` values remain the default; `className` and the
|
|
35
|
+
existing React Native style props are optional overrides that can be combined.
|
|
36
|
+
The initial NativeWind-enabled components are `Radio` and `Button`; additional
|
|
37
|
+
components will expose named class slots as their public styling APIs are added.
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { Button, Radio } from 'neobrutalism-components';
|
|
41
|
+
|
|
42
|
+
<Radio
|
|
43
|
+
className="min-h-14 items-center px-2 py-2"
|
|
44
|
+
label="English"
|
|
45
|
+
selected
|
|
46
|
+
style={{ opacity: 1 }}
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
<Button
|
|
50
|
+
className="bg-sky-500"
|
|
51
|
+
containerClassName="self-end"
|
|
52
|
+
containerStyle={{ marginTop: 12 }}>
|
|
53
|
+
Continue
|
|
54
|
+
</Button>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`className` styles the component surface. Components with a separate outer
|
|
58
|
+
wrapper expose a named class slot—for example, `Button.containerClassName`
|
|
59
|
+
maps to its existing `containerStyle` wrapper.
|
|
60
|
+
|
|
61
|
+
## Example app
|
|
62
|
+
|
|
63
|
+
The repository includes a private Expo Router example application in
|
|
64
|
+
[`example/`](./example). It demonstrates the package in Chats, Calls, Discover,
|
|
65
|
+
and Me screens, while the external `company-ui-test` project remains the focused
|
|
66
|
+
regression consumer. The example is excluded from the npm tarball.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# repository root
|
|
70
|
+
pnpm build
|
|
71
|
+
pnpm --dir example install
|
|
72
|
+
pnpm --dir example start
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
See [`example/README.md`](./example/README.md) for the local workflow and
|
|
76
|
+
validation commands.
|
|
77
|
+
|
|
78
|
+
## Setup
|
|
79
|
+
|
|
80
|
+
Wrap the application once. `SafeAreaProvider` supplies insets used by toasts,
|
|
81
|
+
dialogs, and bottom sheets. `DialogProvider` provides the portal host for
|
|
82
|
+
dialogs, bottom sheets, selects, and menus.
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { useState, type PropsWithChildren } from 'react';
|
|
86
|
+
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
|
87
|
+
import {
|
|
88
|
+
DialogProvider,
|
|
89
|
+
ThemeProvider,
|
|
90
|
+
ToastProvider,
|
|
91
|
+
type ThemeMode,
|
|
92
|
+
} from 'neobrutalism-components';
|
|
93
|
+
|
|
94
|
+
export function AppProviders({ children }: PropsWithChildren) {
|
|
95
|
+
const [mode] = useState<ThemeMode>('light');
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<SafeAreaProvider>
|
|
99
|
+
<ThemeProvider mode={mode}>
|
|
100
|
+
<DialogProvider>
|
|
101
|
+
<ToastProvider>{children}</ToastProvider>
|
|
102
|
+
</DialogProvider>
|
|
103
|
+
</ThemeProvider>
|
|
104
|
+
</SafeAreaProvider>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Theming
|
|
110
|
+
|
|
111
|
+
`ThemeProvider` supports `light` and `dark` modes. Components react to a mode
|
|
112
|
+
change automatically.
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import { useState } from 'react';
|
|
116
|
+
import { Button, ThemeProvider, type ThemeMode } from 'neobrutalism-components';
|
|
117
|
+
|
|
118
|
+
export function ThemeExample() {
|
|
119
|
+
const [mode, setMode] = useState<ThemeMode>('light');
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<ThemeProvider mode={mode}>
|
|
123
|
+
<Button onPress={() => setMode(mode === 'light' ? 'dark' : 'light')}>
|
|
124
|
+
Switch theme
|
|
125
|
+
</Button>
|
|
126
|
+
</ThemeProvider>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Use `useTheme()` inside a component when custom app content needs the same
|
|
132
|
+
tokens:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { Text, View } from 'react-native';
|
|
136
|
+
import { useTheme } from 'neobrutalism-components';
|
|
137
|
+
|
|
138
|
+
export function ThemedPanel() {
|
|
139
|
+
const theme = useTheme();
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<View style={{ backgroundColor: theme.colors.surface, padding: theme.spacing.md }}>
|
|
143
|
+
<Text style={{ color: theme.colors.foreground }}>Custom content</Text>
|
|
144
|
+
</View>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`createTheme`, `theme`, `designTokens`, and `defaultDesignTokenModes` are also
|
|
150
|
+
exported when an application needs to inspect theme data outside a component.
|
|
151
|
+
|
|
152
|
+
### Figma token workflow
|
|
153
|
+
|
|
154
|
+
Figma exports are versioned in [`tokens/figma/`](./tokens/figma). Regenerate
|
|
155
|
+
the runtime TypeScript tokens and the NativeWind-compatible CSS variables after
|
|
156
|
+
updating those files:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
pnpm generate:design-tokens
|
|
160
|
+
pnpm test:design-tokens
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The generated CSS is published as `neobrutalism-components/tokens.css`. A
|
|
164
|
+
NativeWind application can import it once, then preserve its existing semantic
|
|
165
|
+
variables by aliasing them to the generated `--neo-*` variables:
|
|
166
|
+
|
|
167
|
+
```css
|
|
168
|
+
@import 'neobrutalism-components/tokens.css';
|
|
169
|
+
|
|
170
|
+
@layer base {
|
|
171
|
+
:root,
|
|
172
|
+
.dark:root {
|
|
173
|
+
--background: var(--neo-background);
|
|
174
|
+
--primary: var(--neo-primary);
|
|
175
|
+
--color-success-100: var(--neo-success-100);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
This keeps `className` utilities such as `bg-primary` and `bg-success-100`
|
|
181
|
+
working while their values follow the package's light or dark Figma mode.
|
|
182
|
+
|
|
183
|
+
## Components
|
|
184
|
+
|
|
185
|
+
| Group | Components |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| Foundation | `Button`, `IconButton`, `Badge`, `Card`, `Avatar` |
|
|
188
|
+
| Form, selection, and navigation | `Input`, `TextArea`, `PasswordInput`, `UrlInput`, `PhoneInput`, `CurrencyInput`, `OtpInput`, `Checkbox`, `Radio`, `Switch`, `Calendar`, `AppBar`, `BottomNavigation`, `Tabs`, `Accordion`, `Dropdown`, `DropdownMenu`, `ListItem`, `ChatListItem`, `CallItem` |
|
|
189
|
+
| Feedback | `Toast`, `ToastProvider`, `useToast`, `Progress`, `Skeleton` |
|
|
190
|
+
| Overlays | `Dialog`, `ConfirmDialog`, `BottomSheet` |
|
|
191
|
+
|
|
192
|
+
### Button
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
import { Text } from 'react-native';
|
|
196
|
+
import { Button } from 'neobrutalism-components';
|
|
197
|
+
|
|
198
|
+
<Button
|
|
199
|
+
accessibilityLabel="Continue"
|
|
200
|
+
leadingIcon={<Text>←</Text>}
|
|
201
|
+
onPress={handleContinue}
|
|
202
|
+
rounded="sharp"
|
|
203
|
+
trailingIcon={<Text>→</Text>}
|
|
204
|
+
variant="primary">
|
|
205
|
+
Continue
|
|
206
|
+
</Button>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
| Key prop | Description |
|
|
210
|
+
| --- | --- |
|
|
211
|
+
| `variant` | `primary`, `secondary`, `ghost`, `outline`, `destructive`, `warning`, `success`, `info`, `neutral`, or `noShadow`. |
|
|
212
|
+
| `size` | `sm`, `md` (default), or `lg`. |
|
|
213
|
+
| `radius` | Overrides the theme border radius. |
|
|
214
|
+
| `rounded` | `rounded` (default) uses the theme radius; `sharp` sets the surface radius to `0`. An explicit `radius` takes precedence. |
|
|
215
|
+
| `labelStyle` | Text-style override for the label, including a custom `fontSize`. |
|
|
216
|
+
| `leadingIcon` / `trailingIcon` | React nodes rendered alongside the button label. |
|
|
217
|
+
| `containerClassName` / `containerStyle` | NativeWind or React Native styles for the outer shadow container. |
|
|
218
|
+
| `className` / `style` | NativeWind or React Native styles for the pressable surface. |
|
|
219
|
+
|
|
220
|
+
Disabled buttons preserve their visual structure and reduce opacity. Press feedback is
|
|
221
|
+
package-managed. On web, hover uses the Figma hover treatment: primary becomes
|
|
222
|
+
`primaryHover`, ghost uses the accent surface, and shadowed variants extend their
|
|
223
|
+
hard shadow to twice the normal offset. Native platforms do not apply hover state.
|
|
224
|
+
|
|
225
|
+
### IconButton
|
|
226
|
+
|
|
227
|
+
`IconButton` is a square action surface for call controls, compact toolbars, and
|
|
228
|
+
other icon-first actions. The application supplies the icon and behavior.
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
import { MicOff, PhoneOff } from 'lucide-react-native';
|
|
232
|
+
import { IconButton } from 'neobrutalism-components';
|
|
233
|
+
|
|
234
|
+
<IconButton
|
|
235
|
+
accessibilityLabel="Mute microphone"
|
|
236
|
+
icon={<MicOff size={26} />}
|
|
237
|
+
label="Mute"
|
|
238
|
+
onPress={toggleMute}
|
|
239
|
+
/>
|
|
240
|
+
|
|
241
|
+
<IconButton
|
|
242
|
+
accessibilityLabel="End call"
|
|
243
|
+
icon={<PhoneOff size={26} />}
|
|
244
|
+
label="End call"
|
|
245
|
+
onPress={endCall}
|
|
246
|
+
variant="destructive"
|
|
247
|
+
/>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
| Key prop | Description |
|
|
251
|
+
| --- | --- |
|
|
252
|
+
| `icon` / `accessibilityLabel` | Required visual and accessible description. |
|
|
253
|
+
| `label` | Optional copy displayed below the square icon surface. |
|
|
254
|
+
| `variant` | `default`, `primary`, or `destructive`. |
|
|
255
|
+
| `size` | Side length of the icon surface; defaults to `60`. |
|
|
256
|
+
| `rounded` / `radius` | `rounded` uses the theme radius, `sharp` uses `0`; an explicit `radius` takes precedence. |
|
|
257
|
+
| `shadow` / `shadowColor` | Hard shadow is enabled by default. Disable it or override its color when needed. |
|
|
258
|
+
|
|
259
|
+
### ListItem
|
|
260
|
+
|
|
261
|
+
`ListItem` provides a settings or menu row; collection rendering and backend
|
|
262
|
+
data mapping remain in the consuming application.
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
import { useState } from 'react';
|
|
266
|
+
import { Text } from 'react-native';
|
|
267
|
+
import { Avatar, ListItem, Switch } from 'neobrutalism-components';
|
|
268
|
+
|
|
269
|
+
const [notificationsEnabled, setNotificationsEnabled] = useState(true);
|
|
270
|
+
|
|
271
|
+
<ListItem
|
|
272
|
+
description="Manage alerts and reminders"
|
|
273
|
+
leading={<Text>⚙</Text>}
|
|
274
|
+
title="Notifications"
|
|
275
|
+
trailing={
|
|
276
|
+
<Switch
|
|
277
|
+
onValueChange={setNotificationsEnabled}
|
|
278
|
+
value={notificationsEnabled}
|
|
279
|
+
/>
|
|
280
|
+
}
|
|
281
|
+
/>
|
|
282
|
+
|
|
283
|
+
<ListItem
|
|
284
|
+
divider={false}
|
|
285
|
+
onPress={openProfile}
|
|
286
|
+
title="Profile"
|
|
287
|
+
trailingAction={{
|
|
288
|
+
accessibilityLabel: 'Edit profile',
|
|
289
|
+
icon={<Text>›</Text>},
|
|
290
|
+
onPress: openEditProfile,
|
|
291
|
+
}}
|
|
292
|
+
/>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
| Key prop | Description |
|
|
296
|
+
| --- | --- |
|
|
297
|
+
| `title` / `description` | Required primary label and optional supporting label. |
|
|
298
|
+
| `leading` / `leadingStyle` | Optional icon, avatar, or custom content. The slot follows its content size by default; use `leadingStyle` only for alignment or explicit constraints. |
|
|
299
|
+
| `trailing` / `trailingStyle` | Optional consumer-owned content, for example a `Switch`, `Badge`, or chevron. Its slot also follows content size and can be styled independently. |
|
|
300
|
+
| `gap` | Horizontal space between leading, labels, and trailing. Defaults to `theme.spacing.sm`. |
|
|
301
|
+
| `trailingAction` | Optional built-in icon action with a separate accessible press target. It is not rendered when `trailing` is supplied. |
|
|
302
|
+
| `onPress` / `disabled` | Makes the row pressable and controls its disabled state. |
|
|
303
|
+
| `divider` | Defaults to `true`; set to `false` for the final item in a section. |
|
|
304
|
+
| Style props | `className` maps to the row and `containerClassName` maps to its full-width wrapper. React Native overrides remain available through `containerStyle`, `style`, `titleStyle`, and `descriptionStyle`. |
|
|
305
|
+
|
|
306
|
+
Use the same primitive for the Contact Item pattern:
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
<ListItem
|
|
310
|
+
description="ID 99812645"
|
|
311
|
+
divider={false}
|
|
312
|
+
leading={<Avatar initials="AK" size="lg" />}
|
|
313
|
+
style={{ minHeight: 70, paddingVertical: 12 }}
|
|
314
|
+
title="Ariana Kim"
|
|
315
|
+
/>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Divider
|
|
319
|
+
|
|
320
|
+
`Divider` provides a theme-aware horizontal separator for consumer-owned
|
|
321
|
+
sections. It has no shadow or border of its own, and uses `theme.border.color`
|
|
322
|
+
at one pixel by default.
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
import { Divider } from 'neobrutalism-components';
|
|
326
|
+
|
|
327
|
+
<Divider />
|
|
328
|
+
<Divider inset="md" />
|
|
329
|
+
<Divider thickness={2} className="opacity-60" />
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
| Key prop | Description |
|
|
333
|
+
| --- | --- |
|
|
334
|
+
| `inset` | Horizontal inset: `none` (default), `sm`, `md`, or `lg`. |
|
|
335
|
+
| `thickness` | Divider height in pixels; defaults to `1`. |
|
|
336
|
+
| `className` / `style` | Optional NativeWind and React Native style overrides. |
|
|
337
|
+
|
|
338
|
+
### ChatListItem
|
|
339
|
+
|
|
340
|
+
`ChatListItem` provides the chat-list layout: contact content, a timestamp,
|
|
341
|
+
message preview, and an unread counter. Data mapping, navigation, avatar choice,
|
|
342
|
+
and selection state stay in the app.
|
|
343
|
+
|
|
344
|
+
```tsx
|
|
345
|
+
import { Avatar, ChatListItem } from 'neobrutalism-components';
|
|
346
|
+
|
|
347
|
+
<ChatListItem
|
|
348
|
+
leading={<Avatar initials="AK" size="md" />}
|
|
349
|
+
message="See you shortly"
|
|
350
|
+
messageNumberOfLines={2}
|
|
351
|
+
onLongPress={startConversationSelection}
|
|
352
|
+
onPress={openConversation}
|
|
353
|
+
state="unread"
|
|
354
|
+
timestamp="10:11"
|
|
355
|
+
title="Ariana Kim"
|
|
356
|
+
unreadBadgeProps={{ radius: 8, shadowColor: '#01ABFF' }}
|
|
357
|
+
unreadCount={2}
|
|
358
|
+
/>
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
| Key prop | Description |
|
|
362
|
+
| --- | --- |
|
|
363
|
+
| `state` | `default`, `unread`, or `request`; `unread` displays `unreadCount`. |
|
|
364
|
+
| `selectable` / `selected` / `onSelectionChange` | Opt into the Figma selection checkbox while keeping selected state controlled by the application. |
|
|
365
|
+
| `leading` / `leadingStyle` | Optional avatar or custom 40px leading content, with a style override. |
|
|
366
|
+
| `title`, `message`, `timestamp` | Required consumer-owned chat content. |
|
|
367
|
+
| `titleNumberOfLines`, `messageNumberOfLines`, `timestampNumberOfLines` | Per-field truncation limits. The default title limit is `1` (`2` for `request`), while message and timestamp default to `1`. |
|
|
368
|
+
| `unreadBadgeProps` | Optional `Badge` props for the unread counter, such as `radius`, `rounded`, `shadow`, and `shadowColor`. |
|
|
369
|
+
| `divider`, `onPress`, `onLongPress`, `disabled` | Row behavior and section composition. `onLongPress` is useful for consumer-owned multiselect flows. |
|
|
370
|
+
|
|
371
|
+
### CallItem
|
|
372
|
+
|
|
373
|
+
`CallItem` is a call-history row. It controls only the status color; the app
|
|
374
|
+
provides its avatar, phone/video icon, copy, and interaction.
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
import { Phone } from 'lucide-react-native';
|
|
378
|
+
import { Avatar, CallItem } from 'neobrutalism-components';
|
|
379
|
+
|
|
380
|
+
<CallItem
|
|
381
|
+
detail="Incoming · 5:23"
|
|
382
|
+
leading={<Avatar initials="AK" size="lg" />}
|
|
383
|
+
onPress={openCallDetails}
|
|
384
|
+
title="Ariana Kim"
|
|
385
|
+
trailing={<Phone size={24} />}
|
|
386
|
+
type="incoming"
|
|
387
|
+
/>
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
| Key prop | Description |
|
|
391
|
+
| --- | --- |
|
|
392
|
+
| `type` | `incoming`, `outgoing`, `missed`, or `video`; controls the detail color. |
|
|
393
|
+
| `leading` / `leadingStyle` | Optional avatar or custom 48px leading content. |
|
|
394
|
+
| `trailing` | Optional phone, video, or other consumer-owned trailing content. |
|
|
395
|
+
| `title` / `detail` | Required call copy supplied by the application. |
|
|
396
|
+
|
|
397
|
+
### Badge
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
import { Badge } from 'neobrutalism-components';
|
|
401
|
+
|
|
402
|
+
<Badge size="md" type="success">Published</Badge>
|
|
403
|
+
|
|
404
|
+
<Badge className="min-h-8 px-2" shadow={false} size="md" type="primary">
|
|
405
|
+
Category
|
|
406
|
+
</Badge>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
| Key prop | Description |
|
|
410
|
+
| --- | --- |
|
|
411
|
+
| `type` | `default`, `primary`, `info`, `error`, `warning`, or `success`. |
|
|
412
|
+
| `size` | `sm` or `md`. |
|
|
413
|
+
| `radius` / `rounded` | `radius` has priority; otherwise `rounded` is `rounded` (default) or `sharp`. |
|
|
414
|
+
| `shadow` / `shadowColor` | Show or hide the hard shadow, and optionally override its color. |
|
|
415
|
+
| `className` | NativeWind classes mapped to the Badge visual style surface. |
|
|
416
|
+
| `leadingIcon` | Optional icon before the badge label. |
|
|
417
|
+
|
|
418
|
+
### Progress
|
|
419
|
+
|
|
420
|
+
`Progress` represents a determinate value supplied by the consuming
|
|
421
|
+
application or backend.
|
|
422
|
+
|
|
423
|
+
```tsx
|
|
424
|
+
import { Progress } from 'neobrutalism-components';
|
|
425
|
+
|
|
426
|
+
<Progress value={75} />
|
|
427
|
+
|
|
428
|
+
<Progress type="circular" value={75} />
|
|
429
|
+
|
|
430
|
+
<Progress
|
|
431
|
+
accessibilityLabel="Upload progress"
|
|
432
|
+
max={240}
|
|
433
|
+
value={160}
|
|
434
|
+
/>
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
| Key prop | Description |
|
|
438
|
+
| --- | --- |
|
|
439
|
+
| `value` / `max` | Required determinate value and optional maximum (`100` by default). Values are clamped to the valid range. |
|
|
440
|
+
| `type` | `bar` (default) or `circular`. |
|
|
441
|
+
| `showLabel` | Defaults to `false` for bars and `true` for circular indicators. |
|
|
442
|
+
| `size` / `barHeight` | Circular diameter (default `80`) or bar height (default `15`). |
|
|
443
|
+
| `containerStyle` / `style` | Styles for the shadow wrapper or visible track/surface. Use `containerStyle={{ width: 240 }}` to constrain a bar. |
|
|
444
|
+
| `trackStyle` / `indicatorStyle` / `labelStyle` | Style slots for the track, bar fill, and circular label. `indicatorStyle` applies to bars; circular arcs stay theme-driven. |
|
|
445
|
+
|
|
446
|
+
The component is determinate only. The circular arc is drawn dynamically using
|
|
447
|
+
`react-native-svg`, so it reflects any normalized value rather than a static
|
|
448
|
+
75% asset.
|
|
449
|
+
|
|
450
|
+
### Skeleton
|
|
451
|
+
|
|
452
|
+
`Skeleton` provides Figma-matched loading placeholders for chat content, lists,
|
|
453
|
+
and text blocks. It uses a lightweight native opacity pulse and requires no
|
|
454
|
+
additional animation or gradient dependency.
|
|
455
|
+
|
|
456
|
+
```tsx
|
|
457
|
+
import { Skeleton } from 'neobrutalism-components';
|
|
458
|
+
|
|
459
|
+
<Skeleton type="chat" />
|
|
460
|
+
|
|
461
|
+
<Skeleton rounded="rounded" type="list" />
|
|
462
|
+
|
|
463
|
+
<Skeleton animated={false} shadow={false} type="textBlock" />
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
| Key prop | Description |
|
|
467
|
+
| --- | --- |
|
|
468
|
+
| `type` | `textBlock` (default), `list`, or `chat`. Each renders the corresponding Figma placeholder layout. |
|
|
469
|
+
| `rounded` / `radius` | `sharp` is the default; `rounded` uses the theme radius. An explicit `radius` takes precedence. |
|
|
470
|
+
| `shadow` / `shadowColor` / `shadowOffset` | Hard shadow is on by default. Override its color or right-and-bottom offset as needed. |
|
|
471
|
+
| `animated` / `animationDuration` | Enables the native opacity pulse by default. Set `animated={false}` for static loading states. |
|
|
472
|
+
| `style` | Styles the outer preset container, such as its width or placement. |
|
|
473
|
+
|
|
474
|
+
The Figma placeholder uses a gradient. This implementation intentionally uses a
|
|
475
|
+
theme-driven pulse instead, so consumers do not need an extra native gradient
|
|
476
|
+
dependency.
|
|
477
|
+
|
|
478
|
+
### Calendar
|
|
479
|
+
|
|
480
|
+
`Calendar` is an inline, theme-aware calendar for single dates and date
|
|
481
|
+
ranges. Every public value is an ISO calendar date (`YYYY-MM-DD`); it does not
|
|
482
|
+
exchange JavaScript `Date` objects with the consuming application.
|
|
483
|
+
|
|
484
|
+
```tsx
|
|
485
|
+
import { useState } from 'react';
|
|
486
|
+
import { Calendar, type CalendarRangeValue } from 'neobrutalism-components';
|
|
487
|
+
|
|
488
|
+
const [date, setDate] = useState<string | undefined>('2026-08-31');
|
|
489
|
+
const [range, setRange] = useState<CalendarRangeValue | undefined>({
|
|
490
|
+
startDate: '2026-08-04',
|
|
491
|
+
endDate: '2026-08-07',
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
<Calendar
|
|
495
|
+
disabledDates={['2026-09-01']}
|
|
496
|
+
onValueChange={setDate}
|
|
497
|
+
value={date}
|
|
498
|
+
/>
|
|
499
|
+
|
|
500
|
+
<Calendar
|
|
501
|
+
mode="range"
|
|
502
|
+
onValueChange={setRange}
|
|
503
|
+
value={range}
|
|
504
|
+
/>
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
| Key prop | Description |
|
|
508
|
+
| --- | --- |
|
|
509
|
+
| `mode` | `single` (default) or `range`. In range mode, the first press selects a start date and the second selects its end. |
|
|
510
|
+
| `value` / `defaultValue` | Controlled or uncontrolled ISO date value. Use `CalendarRangeValue` (`{ startDate, endDate? }`) for `range` mode. |
|
|
511
|
+
| `minDate` / `maxDate` / `disabledDates` | ISO date bounds and explicitly unavailable dates; blocked dates cannot be selected. |
|
|
512
|
+
| `defaultVisibleMonth` / `onVisibleMonthChange` | Set the initial visible month and observe month navigation using `YYYY-MM-01`. |
|
|
513
|
+
| `firstDayOfWeek` / `locale` | Start the week on Sunday (`0`, default) or Monday (`1`), and format the header/week labels for a locale. |
|
|
514
|
+
| `radius` | A number or `{ topLeft, topRight, bottomRight, bottomLeft }`. Defaults to the theme base radius and is applied to both the calendar surface and its hard shadow. |
|
|
515
|
+
| `selectedRadius` | Radius for a selected single date or selected range days. Defaults to the theme base radius. |
|
|
516
|
+
| `navigationButtonRadius` | Radius for previous/next month buttons and their hard shadows. Defaults to the theme base radius. |
|
|
517
|
+
| `containerStyle` / `style` | Style the shadow wrapper or visible 300px calendar surface. |
|
|
518
|
+
|
|
519
|
+
In range mode, the start and end dates use the secondary/black treatment, while
|
|
520
|
+
the dates between them use the primary blue treatment. Disabled and outside
|
|
521
|
+
month dates use the muted surface.
|
|
522
|
+
|
|
523
|
+
### Accordion
|
|
524
|
+
|
|
525
|
+
`Accordion` manages which sections are expanded. Item content remains owned by
|
|
526
|
+
the consuming application.
|
|
527
|
+
|
|
528
|
+
```tsx
|
|
529
|
+
import { Text } from 'react-native';
|
|
530
|
+
import { Accordion } from 'neobrutalism-components';
|
|
531
|
+
|
|
532
|
+
<Accordion
|
|
533
|
+
defaultValue="overview"
|
|
534
|
+
items={[
|
|
535
|
+
{
|
|
536
|
+
content: <Text>Content supplied by the app.</Text>,
|
|
537
|
+
title: 'Product overview',
|
|
538
|
+
value: 'overview',
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
content: <Text>Feature details supplied by the app.</Text>,
|
|
542
|
+
title: 'Key features',
|
|
543
|
+
value: 'features',
|
|
544
|
+
},
|
|
545
|
+
]}
|
|
546
|
+
shadowVariant="primary"
|
|
547
|
+
/>
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
Use `type="multiple"` when several sections may remain open:
|
|
551
|
+
|
|
552
|
+
```tsx
|
|
553
|
+
<Accordion
|
|
554
|
+
defaultValue={['overview', 'features']}
|
|
555
|
+
items={items}
|
|
556
|
+
onValueChange={setExpandedValues}
|
|
557
|
+
type="multiple"
|
|
558
|
+
/>
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
| Key prop | Description |
|
|
562
|
+
| --- | --- |
|
|
563
|
+
| `items` | `AccordionItem[]` with `value`, `title`, `content`, and optional `disabled`, `leading`, and `trailing`. |
|
|
564
|
+
| `type` | `single` (default) opens one section, or `multiple` keeps any number open. |
|
|
565
|
+
| `value` / `defaultValue` / `onValueChange` | Controlled or uncontrolled expanded state. Single mode uses a string or `null`; multiple mode uses a string array. |
|
|
566
|
+
| `collapsible` | Defaults to `true`; set `false` to keep the expanded item open in single mode. |
|
|
567
|
+
| `shadowVariant` | `default`, `primary`, `secondary`, `destructive`, `warning`, `success`, or `info`; it applies to expanded sections only. |
|
|
568
|
+
| `shadowColor` | Direct hard-shadow override; takes precedence over `shadowVariant`. |
|
|
569
|
+
| Style props | `containerStyle`, `style`, `headerStyle`, `contentStyle`, and `titleStyle`. |
|
|
570
|
+
|
|
571
|
+
Expanded headers use the current theme's `primaryHover` color. The chevron,
|
|
572
|
+
border, hard shadow, and disabled state are package-managed.
|
|
573
|
+
|
|
574
|
+
### Input
|
|
575
|
+
|
|
576
|
+
```tsx
|
|
577
|
+
import { useState } from 'react';
|
|
578
|
+
import { Text } from 'react-native';
|
|
579
|
+
import { Input } from 'neobrutalism-components';
|
|
580
|
+
|
|
581
|
+
const [email, setEmail] = useState('');
|
|
582
|
+
|
|
583
|
+
<Input
|
|
584
|
+
error={email.length === 0 ? 'Email is required.' : undefined}
|
|
585
|
+
helperText="We will only use this for account updates."
|
|
586
|
+
label="Email"
|
|
587
|
+
leadingIcon={<Text>@</Text>}
|
|
588
|
+
onChangeText={setEmail}
|
|
589
|
+
placeholder="you@company.com"
|
|
590
|
+
trailingIcon={<Text>✓</Text>}
|
|
591
|
+
value={email}
|
|
592
|
+
/>
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
| Key prop | Description |
|
|
596
|
+
| --- | --- |
|
|
597
|
+
| `label` | Text shown above the field. |
|
|
598
|
+
| `helperText` | Supporting copy shown below the field when no error is present. |
|
|
599
|
+
| `variant` | `default` (shadowed), `filled` (solid and shadowless), or `dashed` (editable dashed and shadowless). |
|
|
600
|
+
| `error` | Error message; it uses a destructive solid border without a shadow. |
|
|
601
|
+
| `readOnly` | Shows a non-editable, read-only field. |
|
|
602
|
+
| `editable={false}` | Shows a disabled dashed field without a shadow. |
|
|
603
|
+
| `leadingIcon` / `trailingIcon` | React nodes placed inside the field. |
|
|
604
|
+
| `leadingAddon` / `trailingAddon` | Full-height field segments; use them for protocol, currency, or other labeled addons. Their `*Style` counterparts style the segment. |
|
|
605
|
+
| `fieldStyle` | Style slot for the visible input field, for example a custom height or radius. |
|
|
606
|
+
| `changeShadowOnFocus` | **iOS only.** Defaults to `false`, so focus changes only the border. Set to `true` to use the primary color for the hard shadow while focused. Android always keeps the hard shadow static to prevent native focus loss. |
|
|
607
|
+
| Text input props | Native `TextInputProps`, including `value`, `defaultValue`, `editable`, and `onChangeText`. |
|
|
608
|
+
|
|
609
|
+
### TextArea
|
|
610
|
+
|
|
611
|
+
`TextArea` uses the same validation, label, helper, icon, read-only, disabled,
|
|
612
|
+
and style props as `Input`, but renders a native multiline editor. It is 80px
|
|
613
|
+
high by default and aligns text at the top.
|
|
614
|
+
|
|
615
|
+
```tsx
|
|
616
|
+
import { useState } from 'react';
|
|
617
|
+
import { TextArea } from 'neobrutalism-components';
|
|
618
|
+
|
|
619
|
+
export function BioField() {
|
|
620
|
+
const [bio, setBio] = useState('');
|
|
621
|
+
|
|
622
|
+
return (
|
|
623
|
+
<TextArea
|
|
624
|
+
helperText="Visible on your public profile."
|
|
625
|
+
height={120}
|
|
626
|
+
label="About you"
|
|
627
|
+
onChangeText={setBio}
|
|
628
|
+
placeholder="Tell us about yourself"
|
|
629
|
+
value={bio}
|
|
630
|
+
/>
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
| Key prop | Description |
|
|
636
|
+
| --- | --- |
|
|
637
|
+
| `height` | Visible field height; defaults to `80`. |
|
|
638
|
+
| `numberOfLines` | Native multiline line count; defaults to `3`. |
|
|
639
|
+
| `fieldStyle` / `style` | Style slots for the field surface and native text editor. |
|
|
640
|
+
|
|
641
|
+
Native text areas do not implement the web-only drag resize handle shown in
|
|
642
|
+
some Figma previews. Use `height` or `fieldStyle` to select the appropriate
|
|
643
|
+
fixed height for the screen.
|
|
644
|
+
|
|
645
|
+
### PasswordInput
|
|
646
|
+
|
|
647
|
+
`PasswordInput` is an `Input` with a Figma-matched trailing `Show` / `Hide`
|
|
648
|
+
surface. Visibility can be controlled by the app or managed internally.
|
|
649
|
+
|
|
650
|
+
```tsx
|
|
651
|
+
import { useState } from 'react';
|
|
652
|
+
import { PasswordInput } from 'neobrutalism-components';
|
|
653
|
+
|
|
654
|
+
const [password, setPassword] = useState('');
|
|
655
|
+
|
|
656
|
+
<PasswordInput
|
|
657
|
+
helperText="Use the Show button to reveal the value."
|
|
658
|
+
label="Password"
|
|
659
|
+
onChangeText={setPassword}
|
|
660
|
+
onVisibilityChange={(visible) => analytics.track('password_visibility', { visible })}
|
|
661
|
+
value={password}
|
|
662
|
+
/>
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
| Key prop | Description |
|
|
666
|
+
| --- | --- |
|
|
667
|
+
| `visible` / `defaultVisible` | Controlled or uncontrolled visibility state; the password is masked by default. |
|
|
668
|
+
| `onVisibilityChange` | Called with the next visibility state after tapping Show or Hide. |
|
|
669
|
+
| `showLabel` / `hideLabel` | Labels for the built-in trailing visibility control. Defaults to `Show` and `Hide`. |
|
|
670
|
+
| `showVisibilityToggle` | Set `false` to omit the built-in control. |
|
|
671
|
+
| `trailingIcon` | Replaces the built-in Show/Hide control with custom content. |
|
|
672
|
+
|
|
673
|
+
### UrlInput
|
|
674
|
+
|
|
675
|
+
`UrlInput` supplies native URL typing defaults and can show a static protocol
|
|
676
|
+
segment without requiring the consuming app to compose it manually.
|
|
677
|
+
|
|
678
|
+
```tsx
|
|
679
|
+
import { UrlInput } from 'neobrutalism-components';
|
|
680
|
+
|
|
681
|
+
<UrlInput
|
|
682
|
+
label="Website"
|
|
683
|
+
onChangeText={setWebsite}
|
|
684
|
+
protocol="https://"
|
|
685
|
+
value={website}
|
|
686
|
+
/>
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
| Key prop | Description |
|
|
690
|
+
| --- | --- |
|
|
691
|
+
| `protocol` | Protocol label; defaults to `https://`. |
|
|
692
|
+
| `showProtocol` | Set `false` to render a plain URL input. |
|
|
693
|
+
| `leadingAddon` | Replaces the default protocol content. |
|
|
694
|
+
| Native defaults | `keyboardType="url"`, `autoCapitalize="none"`, `autoCorrect={false}`, and `autoComplete="url"`. |
|
|
695
|
+
|
|
696
|
+
### PhoneInput
|
|
697
|
+
|
|
698
|
+
`PhoneInput` provides a country-code segment and phone-native text input
|
|
699
|
+
defaults. The app owns country selection and phone-number validation.
|
|
700
|
+
|
|
701
|
+
```tsx
|
|
702
|
+
import { PhoneInput } from 'neobrutalism-components';
|
|
703
|
+
|
|
704
|
+
<PhoneInput
|
|
705
|
+
country={{ dialCode: '+62', isoCode: 'ID' }}
|
|
706
|
+
label="Phone"
|
|
707
|
+
onChangeText={setPhone}
|
|
708
|
+
onCountryPress={openCountryPicker}
|
|
709
|
+
value={phone}
|
|
710
|
+
/>
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
| Key prop | Description |
|
|
714
|
+
| --- | --- |
|
|
715
|
+
| `country` | `{ isoCode, dialCode }`; defaults to `{ isoCode: 'US', dialCode: '+1' }`. |
|
|
716
|
+
| `onCountryPress` | Makes the country segment interactive; use it to open a picker, dialog, or bottom sheet in the app. |
|
|
717
|
+
| `countryAccessibilityLabel` | Accessible label for the country selection control. |
|
|
718
|
+
| Native defaults | `keyboardType="phone-pad"`, `autoComplete="tel"`, and `textContentType="telephoneNumber"`. |
|
|
719
|
+
|
|
720
|
+
### CurrencyInput
|
|
721
|
+
|
|
722
|
+
`CurrencyInput` is a right-aligned numeric field with Figma-matched currency
|
|
723
|
+
segments. Values are intentionally unformatted so the consuming app can apply
|
|
724
|
+
its own locale, decimal, and submission rules.
|
|
725
|
+
|
|
726
|
+
```tsx
|
|
727
|
+
import { CurrencyInput } from 'neobrutalism-components';
|
|
728
|
+
|
|
729
|
+
<CurrencyInput
|
|
730
|
+
currencyCode="IDR"
|
|
731
|
+
label="Amount"
|
|
732
|
+
onChangeText={setAmount}
|
|
733
|
+
symbol="Rp"
|
|
734
|
+
value={amount}
|
|
735
|
+
/>
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
| Key prop | Description |
|
|
739
|
+
| --- | --- |
|
|
740
|
+
| `symbol` | Leading currency symbol; defaults to `$`. |
|
|
741
|
+
| `currencyCode` | Trailing code; defaults to `USD`. |
|
|
742
|
+
| `showSymbol` / `showCurrencyCode` | Set either to `false` to omit its segment. |
|
|
743
|
+
| Native defaults | `keyboardType="decimal-pad"`, placeholder `0.00`, and right-aligned value text. |
|
|
744
|
+
|
|
745
|
+
### OtpInput
|
|
746
|
+
|
|
747
|
+
`OtpInput` renders a six-cell, masked verification code field by default. It
|
|
748
|
+
uses `react-native-confirmation-code-field` internally, preserving SMS-code
|
|
749
|
+
paste behavior on iOS and Android.
|
|
750
|
+
|
|
751
|
+
```tsx
|
|
752
|
+
import { useState } from 'react';
|
|
753
|
+
import { OtpInput } from 'neobrutalism-components';
|
|
754
|
+
|
|
755
|
+
const [code, setCode] = useState('');
|
|
756
|
+
|
|
757
|
+
<OtpInput
|
|
758
|
+
label="Verification code"
|
|
759
|
+
onChangeText={setCode}
|
|
760
|
+
onComplete={verifyCode}
|
|
761
|
+
value={code}
|
|
762
|
+
/>
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
| Key prop | Description |
|
|
766
|
+
| --- | --- |
|
|
767
|
+
| `value` / `defaultValue` / `onChangeText` | Controlled or uncontrolled OTP state. |
|
|
768
|
+
| `cellCount` | Number of cells; defaults to `6`. |
|
|
769
|
+
| `onComplete` | Called once the code fills every cell. |
|
|
770
|
+
| `mask` / `maskSymbol` | Masking is enabled by default and uses `•`; set `mask={false}` to show entered characters. |
|
|
771
|
+
| `error` / `helperText` / `label` | Input copy and validation state. Error cells use the destructive border. |
|
|
772
|
+
| `cellStyle` / `containerStyle` / `labelStyle` | Style slots for individual cells, the component container, and label. |
|
|
773
|
+
| `changeShadowOnFocus` | **iOS only.** Defaults to `false`, so focus changes only the active-cell border. Set to `true` to use the primary color for the focused cell shadow. Android keeps cell shadows static to preserve native input focus. |
|
|
774
|
+
| Native defaults | `keyboardType="number-pad"`, `textContentType="oneTimeCode"`, and platform SMS OTP autocomplete. |
|
|
775
|
+
|
|
776
|
+
### Card
|
|
777
|
+
|
|
778
|
+
`Card` is a layout container. Its content belongs to the consuming app.
|
|
779
|
+
|
|
780
|
+
```tsx
|
|
781
|
+
import { Text } from 'react-native';
|
|
782
|
+
import {
|
|
783
|
+
Button,
|
|
784
|
+
Card,
|
|
785
|
+
CardAction,
|
|
786
|
+
CardContent,
|
|
787
|
+
CardDescription,
|
|
788
|
+
CardFooter,
|
|
789
|
+
CardHeader,
|
|
790
|
+
CardTitle,
|
|
791
|
+
} from 'neobrutalism-components';
|
|
792
|
+
|
|
793
|
+
<Card
|
|
794
|
+
backgroundColor="#FFFFFF"
|
|
795
|
+
radius={{ topLeft: 16, topRight: 16, bottomLeft: 0, bottomRight: 0 }}
|
|
796
|
+
shadowVariant="primary">
|
|
797
|
+
<CardHeader>
|
|
798
|
+
<CardTitle>Project update</CardTitle>
|
|
799
|
+
<CardAction><Text>New</Text></CardAction>
|
|
800
|
+
</CardHeader>
|
|
801
|
+
<CardContent>
|
|
802
|
+
<CardDescription>Content is supplied by the application.</CardDescription>
|
|
803
|
+
</CardContent>
|
|
804
|
+
<CardFooter>
|
|
805
|
+
<Button variant="outline">View details</Button>
|
|
806
|
+
</CardFooter>
|
|
807
|
+
</Card>
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
| Key prop | Description |
|
|
811
|
+
| --- | --- |
|
|
812
|
+
| `backgroundColor` | Overrides the theme surface color. |
|
|
813
|
+
| `radius` | A number or corner object with `topLeft`, `topRight`, `bottomRight`, and `bottomLeft`. |
|
|
814
|
+
| `shadowVariant` | `default`, `primary`, `secondary`, `destructive`, `warning`, `success`, or `info`. |
|
|
815
|
+
| `shadowColor` | Direct hard-shadow override; takes precedence over `shadowVariant`. |
|
|
816
|
+
| `contentStyle` | Overrides the default content layout, for example `{{ padding: 0 }}` for full-bleed list rows. |
|
|
817
|
+
| `containerStyle` / `style` | Styles for the shadow wrapper or card surface. |
|
|
818
|
+
| `containerClassName` / `className` / `contentClassName` | NativeWind equivalents for `containerStyle`, `style`, and `contentStyle`; `contentClassName` is applied last. |
|
|
819
|
+
|
|
820
|
+
### Avatar
|
|
821
|
+
|
|
822
|
+
```tsx
|
|
823
|
+
import { Avatar } from 'neobrutalism-components';
|
|
824
|
+
|
|
825
|
+
<Avatar initials="AK" size="md" />
|
|
826
|
+
<Avatar size="lg" source={{ uri: profileUri }} variant="image" />
|
|
827
|
+
<Avatar showAdd size="lg" variant="icon" />
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
| Key prop | Description |
|
|
831
|
+
| --- | --- |
|
|
832
|
+
| `variant` | `initial` (default), `icon`, or `image`. |
|
|
833
|
+
| `size` | `xs`, `sm`, `md`, `lg`, or `xl`. |
|
|
834
|
+
| `initials` | Fallback copy for the `initial` variant. |
|
|
835
|
+
| `source` | Native `ImageSourcePropType` for the `image` variant. |
|
|
836
|
+
| `showAdd` | Shows the add indicator for `md`, `lg`, and `xl` avatars. |
|
|
837
|
+
| `rounded` | `rounded` (default) uses the theme radius; `sharp` makes the avatar square. |
|
|
838
|
+
| `shadow` / `shadowColor` | Show or hide the hard shadow, and optionally override its color. |
|
|
839
|
+
|
|
840
|
+
### AppBar
|
|
841
|
+
|
|
842
|
+
`AppBar` supplies the screen-header surface; navigation and every interactive
|
|
843
|
+
slot remain owned by the consuming application. Keep the application wrapped
|
|
844
|
+
with `SafeAreaProvider` as shown in [Setup](#setup).
|
|
845
|
+
|
|
846
|
+
```tsx
|
|
847
|
+
import { Text } from 'react-native';
|
|
848
|
+
import { AppBar, Avatar, Checkbox, Tabs } from 'neobrutalism-components';
|
|
849
|
+
|
|
850
|
+
<AppBar>
|
|
851
|
+
<AppBar.Content
|
|
852
|
+
avatar={<Avatar initials="SJ" size="sm" />}
|
|
853
|
+
leading={<Text onPress={goBack}>←</Text>}
|
|
854
|
+
subtitle="Online now"
|
|
855
|
+
title="Sarah Jenkins"
|
|
856
|
+
trailing={<Text onPress={openActions}>⋮</Text>}
|
|
857
|
+
/>
|
|
858
|
+
<AppBar.Bottom>
|
|
859
|
+
<Tabs items={conversationTabs} />
|
|
860
|
+
</AppBar.Bottom>
|
|
861
|
+
</AppBar>
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
Set `safeArea={false}` only when the parent already applies the top safe-area
|
|
865
|
+
inset or when the bar intentionally overlays screen content. Use
|
|
866
|
+
`safeAreaOffset` when a specific screen needs to fine-tune the safe-area gap;
|
|
867
|
+
negative values are clamped at zero:
|
|
868
|
+
|
|
869
|
+
```tsx
|
|
870
|
+
<AppBar backgroundColor="#25A9EB" safeArea={false}>
|
|
871
|
+
<AppBar.Content title="Select items" trailing={<Text>2 selected</Text>} />
|
|
872
|
+
</AppBar>
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
Use `variant="select"` for a bulk-selection header. The selection count stays in
|
|
876
|
+
the flexible title slot, while the select-all control stays pinned to the right:
|
|
877
|
+
|
|
878
|
+
```tsx
|
|
879
|
+
<AppBar>
|
|
880
|
+
<AppBar.Content
|
|
881
|
+
leading={<Text onPress={exitSelection}>←</Text>}
|
|
882
|
+
title={`${selectedIds.size} selected`}
|
|
883
|
+
trailing={
|
|
884
|
+
<Checkbox
|
|
885
|
+
checked={selectedIds.size === items.length}
|
|
886
|
+
label="Select all"
|
|
887
|
+
labelPosition="left"
|
|
888
|
+
onValueChange={setAllSelected}
|
|
889
|
+
style={{ width: 106 }}
|
|
890
|
+
/>
|
|
891
|
+
}
|
|
892
|
+
variant="select"
|
|
893
|
+
/>
|
|
894
|
+
</AppBar>
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
| Key prop | Description |
|
|
898
|
+
| --- | --- |
|
|
899
|
+
| `safeArea` | Defaults to `true`; applies the top device inset through `SafeAreaProvider`. |
|
|
900
|
+
| `safeAreaOffset` | Adds to the top inset when `safeArea` is enabled. Defaults to `0`; the final inset never becomes negative. |
|
|
901
|
+
| `backgroundColor` | Overrides the default primary app-bar surface. |
|
|
902
|
+
| `variant` | `default` (default) or `select`. `select` matches the compact bottom padding of Figma's selection header. |
|
|
903
|
+
| `leading` / `avatar` / `trailing` | Consumer-owned React-node slots in `AppBar.Content`. Supply accessible pressable controls for actions. |
|
|
904
|
+
| `title` / `subtitle` | String or number values receive theme text styling and truncate safely to one line; custom React nodes remain unchanged. |
|
|
905
|
+
| `AppBar.Bottom` | Optional full-width, application-owned content such as `Tabs`, a selection toolbar, or a balance summary. It replaces the root AppBar's bottom border; padding, dividers, and its inner layout remain consumer-owned. It is not a bottom-tab navigator. |
|
|
906
|
+
| Style props | `containerStyle` and `style` on `AppBar`; `containerStyle`, `titleStyle`, and `subtitleStyle` on `AppBar.Content`; `style` on `AppBar.Bottom`. |
|
|
907
|
+
|
|
908
|
+
### Checkbox, Radio, and Switch
|
|
909
|
+
|
|
910
|
+
These controls are controlled by their value props and callbacks.
|
|
911
|
+
|
|
912
|
+
```tsx
|
|
913
|
+
import { useState } from 'react';
|
|
914
|
+
import { Checkbox, Radio, Switch } from 'neobrutalism-components';
|
|
915
|
+
|
|
916
|
+
const [accepted, setAccepted] = useState(false);
|
|
917
|
+
const [plan, setPlan] = useState<'standard' | 'express'>('standard');
|
|
918
|
+
const [notifications, setNotifications] = useState(false);
|
|
919
|
+
|
|
920
|
+
<Checkbox
|
|
921
|
+
checked={accepted}
|
|
922
|
+
description="Required before continuing."
|
|
923
|
+
label="Accept terms"
|
|
924
|
+
onValueChange={setAccepted}
|
|
925
|
+
/>
|
|
926
|
+
|
|
927
|
+
<Radio
|
|
928
|
+
label="Standard delivery"
|
|
929
|
+
onPress={() => setPlan('standard')}
|
|
930
|
+
selected={plan === 'standard'}
|
|
931
|
+
/>
|
|
932
|
+
|
|
933
|
+
<Switch
|
|
934
|
+
label="Notifications"
|
|
935
|
+
onValueChange={setNotifications}
|
|
936
|
+
value={notifications}
|
|
937
|
+
/>
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
| Component | Key props |
|
|
941
|
+
| --- | --- |
|
|
942
|
+
| `Checkbox` | `checked`, `onValueChange`, `indeterminate`, `error`, `radius`, `label`, `description`, `labelPosition`, `disabled`. |
|
|
943
|
+
| `Radio` | `selected`, native `onPress`, `label`, `description`, `labelPosition`, `disabled`, `className`, `style`. |
|
|
944
|
+
| `Switch` | `value`, `onValueChange`, `label`, `description`, `labelPosition`, `disabled`. |
|
|
945
|
+
|
|
946
|
+
`labelPosition` is `right` by default and also accepts `left`.
|
|
947
|
+
|
|
948
|
+
### Tabs
|
|
949
|
+
|
|
950
|
+
`Tabs` is a value selector; it does not render a tab panel. Keep the selected
|
|
951
|
+
screen content in the consuming application.
|
|
952
|
+
|
|
953
|
+
```tsx
|
|
954
|
+
import { useState } from 'react';
|
|
955
|
+
import { Text } from 'react-native';
|
|
956
|
+
import { Badge, Tabs, type TabItem } from 'neobrutalism-components';
|
|
957
|
+
|
|
958
|
+
const items: readonly TabItem[] = [
|
|
959
|
+
{ label: 'Overview', value: 'overview' },
|
|
960
|
+
{
|
|
961
|
+
label: 'Activity',
|
|
962
|
+
leading: <Text>•</Text>,
|
|
963
|
+
trailing: <Badge size="sm">3</Badge>,
|
|
964
|
+
value: 'activity',
|
|
965
|
+
},
|
|
966
|
+
{ disabled: true, label: 'Settings', value: 'settings' },
|
|
967
|
+
];
|
|
968
|
+
|
|
969
|
+
export function ProfileTabs() {
|
|
970
|
+
const [tab, setTab] = useState('overview');
|
|
971
|
+
|
|
972
|
+
return (
|
|
973
|
+
<>
|
|
974
|
+
<Tabs
|
|
975
|
+
border="bottom"
|
|
976
|
+
contentContainerStyle={{ gap: 8, paddingHorizontal: 12 }}
|
|
977
|
+
items={items}
|
|
978
|
+
onValueChange={setTab}
|
|
979
|
+
style={{ borderBottomWidth: 3 }}
|
|
980
|
+
value={tab}
|
|
981
|
+
variant="primary"
|
|
982
|
+
/>
|
|
983
|
+
{tab === 'overview' && <Overview />}
|
|
984
|
+
{tab === 'activity' && <Activity />}
|
|
985
|
+
</>
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
| Key prop | Description |
|
|
991
|
+
| --- | --- |
|
|
992
|
+
| `items` | `TabItem[]` with `value`, `label`, optional `disabled`, `leading`, and `trailing`. |
|
|
993
|
+
| `value` / `onValueChange` | Controlled selected value and callback. |
|
|
994
|
+
| `defaultValue` | Initial value for uncontrolled usage. Without a default, the first enabled item is selected. |
|
|
995
|
+
| `layout` | `scroll` (default) for a horizontally scrollable row, or `equal` for a small fixed set of equal-width tabs. |
|
|
996
|
+
| `variant` | Selected-item appearance: `primary` (default), `secondary`, `destructive`, `warning`, `success`, `info`, or `neutral`. |
|
|
997
|
+
| `border` | Outer rail border: `all` (default), `bottom`, or `none`. Use `style` to override its native border width or color. |
|
|
998
|
+
| `rounded` | Outer rail shape: `sharp`, `rounded` (default), or `pill`. This does not change tab-item shape. |
|
|
999
|
+
| `itemRounded` | `sharp` (default, matching Figma), `rounded`, or `pill`; sets the shape of every tab trigger independently from `rounded`. |
|
|
1000
|
+
| `itemRadius` | Direct tab-trigger radius override. It takes precedence over `itemRounded`. |
|
|
1001
|
+
| `shadow` | Renders the Neo hard shadow for the outer rail. Defaults to `false`. |
|
|
1002
|
+
| Style slots | `containerStyle` for the shadow wrapper, `style` for the rail, `contentContainerStyle` for list padding and gap, `itemStyle` and `selectedItemStyle` for triggers, plus `labelStyle` and `selectedLabelStyle` for labels. |
|
|
1003
|
+
| `accessibilityLabel` | Accessible label for the tab list; defaults to `Tabs`. |
|
|
1004
|
+
|
|
1005
|
+
Each trigger exposes the native `tab` role with selected and disabled state.
|
|
1006
|
+
|
|
1007
|
+
### BottomNavigation
|
|
1008
|
+
|
|
1009
|
+
`BottomNavigation` renders the visual bottom-navigation surface only. It does
|
|
1010
|
+
not know about Expo Router or any other navigation library: keep route state
|
|
1011
|
+
and navigation calls in the consuming application.
|
|
1012
|
+
|
|
1013
|
+
```tsx
|
|
1014
|
+
import { useState } from 'react';
|
|
1015
|
+
import { Text } from 'react-native';
|
|
1016
|
+
import { BottomNavigation } from 'neobrutalism-components';
|
|
1017
|
+
|
|
1018
|
+
export function AppNavigation() {
|
|
1019
|
+
const [tab, setTab] = useState('chats');
|
|
1020
|
+
|
|
1021
|
+
return (
|
|
1022
|
+
<BottomNavigation
|
|
1023
|
+
items={[
|
|
1024
|
+
{
|
|
1025
|
+
activeIcon: <Text style={{ color: '#01ABFF' }}>●</Text>,
|
|
1026
|
+
icon: <Text>○</Text>,
|
|
1027
|
+
label: 'Chats',
|
|
1028
|
+
value: 'chats',
|
|
1029
|
+
},
|
|
1030
|
+
{ icon: <Text>○</Text>, label: 'Calls', value: 'calls' },
|
|
1031
|
+
]}
|
|
1032
|
+
onValueChange={setTab}
|
|
1033
|
+
radius={12}
|
|
1034
|
+
shadowVariant="primary"
|
|
1035
|
+
value={tab}
|
|
1036
|
+
/>
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
| Key prop | Description |
|
|
1042
|
+
| --- | --- |
|
|
1043
|
+
| `items` | A non-empty list of `{ value, label, icon, activeIcon?, disabled?, accessibilityLabel? }`. `activeIcon` is rendered for the selected item when supplied. |
|
|
1044
|
+
| `value` / `onValueChange` | Controlled selected value and callback. Call the app router from `onValueChange` when a tab represents a route. |
|
|
1045
|
+
| `safeArea` | Defaults to `true` and adds the bottom device inset. Pass `false` only when a parent navigator already owns that inset or for an inline preview. |
|
|
1046
|
+
| `radius` | Radius of the bordered navigation surface and hard shadow. Defaults to the theme border radius. |
|
|
1047
|
+
| `indicatorRadius` | Radius for the selected tab's bottom indicator. Defaults to `0` for the square Figma treatment. |
|
|
1048
|
+
| `shadow` | Enables the hard shadow; defaults to `true`. Use `false` for a flat navigation surface. |
|
|
1049
|
+
| `shadowVariant` | `default`, `primary`, `secondary`, `destructive`, `warning`, `success`, or `info`. It defaults to `primary` to match the Figma navigation shadow. |
|
|
1050
|
+
| `shadowColor` | Direct hard-shadow override. It takes precedence over `shadowVariant`. |
|
|
1051
|
+
| `containerStyle` / `style` / `contentStyle` | Style the outer wrapper, safe-area surface, or bordered navigation row. |
|
|
1052
|
+
|
|
1053
|
+
Each item exposes the native `tab` role with selected and disabled state. Use
|
|
1054
|
+
`BottomNavigation` as a custom tab-bar view in the main project rather than
|
|
1055
|
+
rendering it alongside another navigator's own tab bar.
|
|
1056
|
+
|
|
1057
|
+
### Dropdown
|
|
1058
|
+
|
|
1059
|
+
Use `Dropdown` for value selection. `single` is the default; use
|
|
1060
|
+
`searchable` for local filtering and `multi-select` for checkbox selection.
|
|
1061
|
+
|
|
1062
|
+
```tsx
|
|
1063
|
+
import { useState } from 'react';
|
|
1064
|
+
import { Dropdown } from 'neobrutalism-components';
|
|
1065
|
+
|
|
1066
|
+
const [team, setTeam] = useState<string>();
|
|
1067
|
+
|
|
1068
|
+
<Dropdown
|
|
1069
|
+
onValueChange={setTeam}
|
|
1070
|
+
options={[
|
|
1071
|
+
{ label: 'Design', value: 'design' },
|
|
1072
|
+
{ label: 'Engineering', value: 'engineering' },
|
|
1073
|
+
{ disabled: true, label: 'Sales (Full)', value: 'sales' },
|
|
1074
|
+
]}
|
|
1075
|
+
placeholder="Select a team"
|
|
1076
|
+
type="single"
|
|
1077
|
+
value={team}
|
|
1078
|
+
/>
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
`Dropdown` can also normalize data returned from an API. Keep backend models in
|
|
1082
|
+
the application and provide mapper callbacks instead of reshaping every
|
|
1083
|
+
response into UI data first.
|
|
1084
|
+
|
|
1085
|
+
```tsx
|
|
1086
|
+
import { Avatar, Dropdown } from 'neobrutalism-components';
|
|
1087
|
+
|
|
1088
|
+
type Team = {
|
|
1089
|
+
avatarUri?: string;
|
|
1090
|
+
department?: { name: string };
|
|
1091
|
+
id: string;
|
|
1092
|
+
initials: string;
|
|
1093
|
+
isFull: boolean;
|
|
1094
|
+
name: string;
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
declare const teams: Team[];
|
|
1098
|
+
|
|
1099
|
+
<Dropdown
|
|
1100
|
+
getOptionDisabled={(team: Team) => team.isFull}
|
|
1101
|
+
getOptionGroup={(team: Team) => team.department?.name}
|
|
1102
|
+
getOptionLabel={(team: Team) => team.name}
|
|
1103
|
+
getOptionSearchText={(team: Team) => `${team.name} ${team.department?.name ?? ''}`}
|
|
1104
|
+
getOptionValue={(team: Team) => team.id}
|
|
1105
|
+
items={teams}
|
|
1106
|
+
renderLeading={(team: Team) => (
|
|
1107
|
+
<Avatar
|
|
1108
|
+
initials={team.initials}
|
|
1109
|
+
size="xs"
|
|
1110
|
+
source={team.avatarUri ? { uri: team.avatarUri } : undefined}
|
|
1111
|
+
variant={team.avatarUri ? 'image' : 'initial'}
|
|
1112
|
+
/>
|
|
1113
|
+
)}
|
|
1114
|
+
showSelectedLeading
|
|
1115
|
+
type="searchable"
|
|
1116
|
+
/>
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
| Key prop | Description |
|
|
1120
|
+
| --- | --- |
|
|
1121
|
+
| `type` | `single`, `searchable`, or `multi-select`. |
|
|
1122
|
+
| `options` | Normalized `DropdownOption[]` with `label`, `value`, optional `group`, `disabled`, and `leading`. |
|
|
1123
|
+
| `items` | Raw application or backend data. Use it with `getOptionValue` and `getOptionLabel`. |
|
|
1124
|
+
| `getOptionGroup` / `getOptionDisabled` | Optional group label and disabled-state mappers for `items`. |
|
|
1125
|
+
| `getOptionSearchText` | Optional additional searchable text for `single` and `searchable` dropdowns. |
|
|
1126
|
+
| `renderLeading` | Renders an icon or avatar for raw `items`. |
|
|
1127
|
+
| `showSelectedLeading` | Shows the selected leading node in `single` and `searchable` triggers. |
|
|
1128
|
+
| `renderOption` / `renderValue` | Escape hatches for custom rows or trigger values. |
|
|
1129
|
+
| `value` / `defaultValue` | A string for `single` and `searchable`; a string array for `multi-select`. |
|
|
1130
|
+
| `badgeRadius` / `checkboxRadius` | Multi-select tag and checkbox radius overrides. |
|
|
1131
|
+
|
|
1132
|
+
For multi-select, use a string array:
|
|
1133
|
+
|
|
1134
|
+
```tsx
|
|
1135
|
+
const [teamIds, setTeamIds] = useState<readonly string[]>([]);
|
|
1136
|
+
|
|
1137
|
+
<Dropdown
|
|
1138
|
+
onValueChange={setTeamIds}
|
|
1139
|
+
options={teamOptions}
|
|
1140
|
+
type="multi-select"
|
|
1141
|
+
value={teamIds}
|
|
1142
|
+
/>
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
### DropdownMenu
|
|
1146
|
+
|
|
1147
|
+
`DropdownMenu` is an action menu, not a value-select control.
|
|
1148
|
+
|
|
1149
|
+
```tsx
|
|
1150
|
+
import { Button, DropdownMenu } from 'neobrutalism-components';
|
|
1151
|
+
|
|
1152
|
+
<DropdownMenu
|
|
1153
|
+
align="end"
|
|
1154
|
+
items={[
|
|
1155
|
+
{ label: 'Edit', onPress: editProject },
|
|
1156
|
+
{ destructive: true, label: 'Delete', onPress: deleteProject },
|
|
1157
|
+
{ disabled: true, label: 'Share' },
|
|
1158
|
+
]}
|
|
1159
|
+
shadowVariant="primary">
|
|
1160
|
+
<Button variant="outline">Actions</Button>
|
|
1161
|
+
</DropdownMenu>
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
| Key prop | Description |
|
|
1165
|
+
| --- | --- |
|
|
1166
|
+
| `items` | Menu rows with `label`, optional `onPress`, `disabled`, and `destructive`. |
|
|
1167
|
+
| `variant` | `default` for vertical actions or `emoji` for horizontal quick reactions. |
|
|
1168
|
+
| `align` | Trigger alignment: `start`, `center`, or `end`. Defaults to `start`. |
|
|
1169
|
+
| `side` / `sideOffset` | Menu side (`top` or `bottom`) and gap from its trigger. Defaults to `bottom` and `6`. |
|
|
1170
|
+
| `shadow` | Enables the neobrutalist shadow. Defaults to `true`. |
|
|
1171
|
+
| `shadowVariant` | Shadow color token: `default`, `primary`, `secondary`, `destructive`, `warning`, `success`, or `info`. |
|
|
1172
|
+
| `shadowColor` | Explicit shadow color. Takes precedence over `shadowVariant`. |
|
|
1173
|
+
| `shadowOffset` | Right-and-bottom shadow thickness. Defaults to the theme shadow offset. |
|
|
1174
|
+
| `children` | One trigger element, usually a `Button`. |
|
|
1175
|
+
|
|
1176
|
+
### Toast
|
|
1177
|
+
|
|
1178
|
+
Use `useToast` inside `ToastProvider` for application notifications.
|
|
1179
|
+
|
|
1180
|
+
```tsx
|
|
1181
|
+
import { Button, useToast } from 'neobrutalism-components';
|
|
1182
|
+
|
|
1183
|
+
export function SaveButton() {
|
|
1184
|
+
const { show } = useToast();
|
|
1185
|
+
|
|
1186
|
+
return (
|
|
1187
|
+
<Button
|
|
1188
|
+
onPress={() =>
|
|
1189
|
+
show({
|
|
1190
|
+
description: 'Your changes have been saved successfully.',
|
|
1191
|
+
title: 'Success',
|
|
1192
|
+
type: 'success',
|
|
1193
|
+
})
|
|
1194
|
+
}>
|
|
1195
|
+
Save changes
|
|
1196
|
+
</Button>
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
```
|
|
1200
|
+
|
|
1201
|
+
| Key prop or option | Description |
|
|
1202
|
+
| --- | --- |
|
|
1203
|
+
| `ToastProvider position` | `top` (default) or `bottom`. |
|
|
1204
|
+
| `ToastProvider offset` | Optional viewport offset; safe-area insets are used by default. |
|
|
1205
|
+
| `show({ title, description, type })` | Adds a toast of type `success`, `error`, `info`, or `note`. |
|
|
1206
|
+
| `duration` | Milliseconds before dismissal; use `null` to keep it open. |
|
|
1207
|
+
| `action` | Optional `{ label, onPress }` action. |
|
|
1208
|
+
|
|
1209
|
+
`Toast` is also exported for controlled rendering with `open` and
|
|
1210
|
+
`onOpenChange`.
|
|
1211
|
+
|
|
1212
|
+
```tsx
|
|
1213
|
+
import { Toast } from 'neobrutalism-components';
|
|
1214
|
+
|
|
1215
|
+
<Toast
|
|
1216
|
+
description="Your changes have been saved successfully."
|
|
1217
|
+
onOpenChange={setToastOpen}
|
|
1218
|
+
open={toastOpen}
|
|
1219
|
+
title="Success"
|
|
1220
|
+
type="success"
|
|
1221
|
+
/>
|
|
1222
|
+
```
|
|
1223
|
+
|
|
1224
|
+
### Dialog and ConfirmDialog
|
|
1225
|
+
|
|
1226
|
+
`Dialog` is composable. `ConfirmDialog` provides a focused confirmation flow
|
|
1227
|
+
with default or custom actions.
|
|
1228
|
+
|
|
1229
|
+
```tsx
|
|
1230
|
+
import { useState } from 'react';
|
|
1231
|
+
import {
|
|
1232
|
+
Button,
|
|
1233
|
+
Dialog,
|
|
1234
|
+
DialogBody,
|
|
1235
|
+
DialogFooter,
|
|
1236
|
+
DialogHeader,
|
|
1237
|
+
} from 'neobrutalism-components';
|
|
1238
|
+
|
|
1239
|
+
const [open, setOpen] = useState(false);
|
|
1240
|
+
|
|
1241
|
+
<>
|
|
1242
|
+
<Button onPress={() => setOpen(true)}>Edit profile</Button>
|
|
1243
|
+
<Dialog onOpenChange={setOpen} open={open}>
|
|
1244
|
+
<DialogHeader title="Edit profile" titleNumberOfLines={2} />
|
|
1245
|
+
<DialogBody>{/* Application-owned form content */}</DialogBody>
|
|
1246
|
+
<DialogFooter>
|
|
1247
|
+
<Button onPress={() => setOpen(false)} variant="ghost">Cancel</Button>
|
|
1248
|
+
<Button onPress={saveProfile}>Save</Button>
|
|
1249
|
+
</DialogFooter>
|
|
1250
|
+
</Dialog>
|
|
1251
|
+
</>
|
|
1252
|
+
```
|
|
1253
|
+
|
|
1254
|
+
```tsx
|
|
1255
|
+
import { ConfirmDialog } from 'neobrutalism-components';
|
|
1256
|
+
|
|
1257
|
+
<ConfirmDialog
|
|
1258
|
+
confirmLabel="Delete"
|
|
1259
|
+
description="This action cannot be undone."
|
|
1260
|
+
onConfirm={deleteProject}
|
|
1261
|
+
onOpenChange={setDeleteOpen}
|
|
1262
|
+
open={deleteOpen}
|
|
1263
|
+
title="Delete project"
|
|
1264
|
+
/>
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
| Component | Key props |
|
|
1268
|
+
| --- | --- |
|
|
1269
|
+
| `Dialog` | Controlled `open` and `onOpenChange`; compose with `DialogHeader`, `DialogBody`, and `DialogFooter`. |
|
|
1270
|
+
| `DialogHeader` | `title`, optional `closeLabel`, and `titleNumberOfLines` (default `1`). |
|
|
1271
|
+
| `ConfirmDialog` | `title`, `description`, controlled `open`, `onOpenChange`, `onConfirm`, optional `onCancel`, labels, `icon`, and custom `actions`. |
|
|
1272
|
+
|
|
1273
|
+
### BottomSheet
|
|
1274
|
+
|
|
1275
|
+
`BottomSheet` uses the dialog portal and anchors to the screen bottom. It
|
|
1276
|
+
handles keyboard movement; use `keyboardVerticalOffset` only for a persistent
|
|
1277
|
+
screen header that also needs to be cleared.
|
|
1278
|
+
|
|
1279
|
+
```tsx
|
|
1280
|
+
import { useState } from 'react';
|
|
1281
|
+
import { Button, BottomSheet, BottomSheetBody, BottomSheetFooter, BottomSheetHeader } from 'neobrutalism-components';
|
|
1282
|
+
|
|
1283
|
+
const [open, setOpen] = useState(false);
|
|
1284
|
+
|
|
1285
|
+
<>
|
|
1286
|
+
<Button onPress={() => setOpen(true)}>Edit profile</Button>
|
|
1287
|
+
<BottomSheet
|
|
1288
|
+
animation="slide"
|
|
1289
|
+
animationDuration={220}
|
|
1290
|
+
onOpenChange={setOpen}
|
|
1291
|
+
open={open}
|
|
1292
|
+
radius={16}>
|
|
1293
|
+
<BottomSheetHeader title="Update profile" />
|
|
1294
|
+
<BottomSheetBody>{/* Application-owned form content */}</BottomSheetBody>
|
|
1295
|
+
<BottomSheetFooter>
|
|
1296
|
+
<Button onPress={() => setOpen(false)} variant="ghost">Cancel</Button>
|
|
1297
|
+
<Button onPress={saveProfile} variant="warning">Save changes</Button>
|
|
1298
|
+
</BottomSheetFooter>
|
|
1299
|
+
</BottomSheet>
|
|
1300
|
+
</>
|
|
1301
|
+
```
|
|
1302
|
+
|
|
1303
|
+
| Key prop | Description |
|
|
1304
|
+
| --- | --- |
|
|
1305
|
+
| `open` / `onOpenChange` | Controlled visibility. Tapping the backdrop invokes `onOpenChange(false)`. |
|
|
1306
|
+
| `animation` | `slide` (default) or `none`. |
|
|
1307
|
+
| `animationDuration` | Open and close duration in milliseconds; defaults to `220`. |
|
|
1308
|
+
| `radius` | Overrides both top corner radii. |
|
|
1309
|
+
| `shadow` | Shows the platform surface shadow. Defaults to `true`; use `false` for a flat sheet. |
|
|
1310
|
+
| `bottomPadding` | Overrides the safe-area spacing below the sheet content. |
|
|
1311
|
+
| `keyboardVerticalOffset` | Header offset for keyboard positioning; defaults to `0`. |
|
|
1312
|
+
|
|
1313
|
+
## Local Development and Publishing
|
|
1314
|
+
|
|
1315
|
+
The package currently resolves directly to `src/index.ts` and has
|
|
1316
|
+
`"private": true` in `package.json`; this is deliberate for local iteration.
|
|
1317
|
+
|
|
1318
|
+
Before publishing to npm, add an intentional release configuration that:
|
|
1319
|
+
|
|
1320
|
+
1. builds JavaScript output rather than shipping TypeScript source only;
|
|
1321
|
+
2. emits `.d.ts` declaration files and points `types`/`exports` to them;
|
|
1322
|
+
3. removes `private`, chooses a version, and verifies the npm tarball; and
|
|
1323
|
+
4. documents the supported React Native and Expo ranges for that release.
|
|
1324
|
+
|
|
1325
|
+
Until then, consume the package through the local file dependency and run the
|
|
1326
|
+
package typecheck after API changes:
|
|
1327
|
+
|
|
1328
|
+
```bash
|
|
1329
|
+
pnpm run typecheck
|
|
1330
|
+
```
|