@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.
Files changed (154) hide show
  1. package/README.md +1330 -0
  2. package/dist/module/assets/avatar-profile.png +0 -0
  3. package/dist/module/components/accordion/accordion.js +203 -0
  4. package/dist/module/components/accordion/accordion.js.map +1 -0
  5. package/dist/module/components/app-bar/app-bar.js +140 -0
  6. package/dist/module/components/app-bar/app-bar.js.map +1 -0
  7. package/dist/module/components/avatar/avatar.js +184 -0
  8. package/dist/module/components/avatar/avatar.js.map +1 -0
  9. package/dist/module/components/badge/badge.js +133 -0
  10. package/dist/module/components/badge/badge.js.map +1 -0
  11. package/dist/module/components/bottom-navigation/bottom-navigation.js +166 -0
  12. package/dist/module/components/bottom-navigation/bottom-navigation.js.map +1 -0
  13. package/dist/module/components/bottom-sheet/bottom-sheet.js +285 -0
  14. package/dist/module/components/bottom-sheet/bottom-sheet.js.map +1 -0
  15. package/dist/module/components/button/button.js +253 -0
  16. package/dist/module/components/button/button.js.map +1 -0
  17. package/dist/module/components/calendar/calendar.js +458 -0
  18. package/dist/module/components/calendar/calendar.js.map +1 -0
  19. package/dist/module/components/call-item/call-item.js +128 -0
  20. package/dist/module/components/call-item/call-item.js.map +1 -0
  21. package/dist/module/components/card/card.js +189 -0
  22. package/dist/module/components/card/card.js.map +1 -0
  23. package/dist/module/components/chat-list-item/chat-list-item.js +212 -0
  24. package/dist/module/components/chat-list-item/chat-list-item.js.map +1 -0
  25. package/dist/module/components/checkbox/checkbox.js +141 -0
  26. package/dist/module/components/checkbox/checkbox.js.map +1 -0
  27. package/dist/module/components/dialog/dialog.js +342 -0
  28. package/dist/module/components/dialog/dialog.js.map +1 -0
  29. package/dist/module/components/divider/divider.js +41 -0
  30. package/dist/module/components/divider/divider.js.map +1 -0
  31. package/dist/module/components/dropdown/dropdown-menu.js +214 -0
  32. package/dist/module/components/dropdown/dropdown-menu.js.map +1 -0
  33. package/dist/module/components/dropdown/dropdown.js +616 -0
  34. package/dist/module/components/dropdown/dropdown.js.map +1 -0
  35. package/dist/module/components/dropdown/types.js +4 -0
  36. package/dist/module/components/dropdown/types.js.map +1 -0
  37. package/dist/module/components/icon-button/icon-button.js +140 -0
  38. package/dist/module/components/icon-button/icon-button.js.map +1 -0
  39. package/dist/module/components/input/currency-input.js +60 -0
  40. package/dist/module/components/input/currency-input.js.map +1 -0
  41. package/dist/module/components/input/input.js +281 -0
  42. package/dist/module/components/input/input.js.map +1 -0
  43. package/dist/module/components/input/otp-input.js +196 -0
  44. package/dist/module/components/input/otp-input.js.map +1 -0
  45. package/dist/module/components/input/password-input.js +74 -0
  46. package/dist/module/components/input/password-input.js.map +1 -0
  47. package/dist/module/components/input/phone-input.js +68 -0
  48. package/dist/module/components/input/phone-input.js.map +1 -0
  49. package/dist/module/components/input/url-input.js +43 -0
  50. package/dist/module/components/input/url-input.js.map +1 -0
  51. package/dist/module/components/list-item/list-item.js +148 -0
  52. package/dist/module/components/list-item/list-item.js.map +1 -0
  53. package/dist/module/components/pin-code-field/pin-code-field.js +164 -0
  54. package/dist/module/components/pin-code-field/pin-code-field.js.map +1 -0
  55. package/dist/module/components/progress/progress.js +261 -0
  56. package/dist/module/components/progress/progress.js.map +1 -0
  57. package/dist/module/components/radio/radio.js +135 -0
  58. package/dist/module/components/radio/radio.js.map +1 -0
  59. package/dist/module/components/skeleton/skeleton.js +328 -0
  60. package/dist/module/components/skeleton/skeleton.js.map +1 -0
  61. package/dist/module/components/switch/switch.js +170 -0
  62. package/dist/module/components/switch/switch.js.map +1 -0
  63. package/dist/module/components/tabs/tabs.js +197 -0
  64. package/dist/module/components/tabs/tabs.js.map +1 -0
  65. package/dist/module/components/toast/toast-provider.js +117 -0
  66. package/dist/module/components/toast/toast-provider.js.map +1 -0
  67. package/dist/module/components/toast/toast.js +154 -0
  68. package/dist/module/components/toast/toast.js.map +1 -0
  69. package/dist/module/index.js +38 -0
  70. package/dist/module/index.js.map +1 -0
  71. package/dist/module/package.json +1 -0
  72. package/dist/module/theme/design-tokens.generated.js +379 -0
  73. package/dist/module/theme/design-tokens.generated.js.map +1 -0
  74. package/dist/module/theme/theme-provider.js +20 -0
  75. package/dist/module/theme/theme-provider.js.map +1 -0
  76. package/dist/module/theme/theme.js +56 -0
  77. package/dist/module/theme/theme.js.map +1 -0
  78. package/dist/typescript/components/accordion/accordion.d.ts +40 -0
  79. package/dist/typescript/components/accordion/accordion.d.ts.map +1 -0
  80. package/dist/typescript/components/app-bar/app-bar.d.ts +34 -0
  81. package/dist/typescript/components/app-bar/app-bar.d.ts.map +1 -0
  82. package/dist/typescript/components/avatar/avatar.d.ts +17 -0
  83. package/dist/typescript/components/avatar/avatar.d.ts.map +1 -0
  84. package/dist/typescript/components/badge/badge.d.ts +20 -0
  85. package/dist/typescript/components/badge/badge.d.ts.map +1 -0
  86. package/dist/typescript/components/bottom-navigation/bottom-navigation.d.ts +28 -0
  87. package/dist/typescript/components/bottom-navigation/bottom-navigation.d.ts.map +1 -0
  88. package/dist/typescript/components/bottom-sheet/bottom-sheet.d.ts +38 -0
  89. package/dist/typescript/components/bottom-sheet/bottom-sheet.d.ts.map +1 -0
  90. package/dist/typescript/components/button/button.d.ts +21 -0
  91. package/dist/typescript/components/button/button.d.ts.map +1 -0
  92. package/dist/typescript/components/calendar/calendar.d.ts +41 -0
  93. package/dist/typescript/components/calendar/calendar.d.ts.map +1 -0
  94. package/dist/typescript/components/call-item/call-item.d.ts +21 -0
  95. package/dist/typescript/components/call-item/call-item.d.ts.map +1 -0
  96. package/dist/typescript/components/card/card.d.ts +37 -0
  97. package/dist/typescript/components/card/card.d.ts.map +1 -0
  98. package/dist/typescript/components/chat-list-item/chat-list-item.d.ts +37 -0
  99. package/dist/typescript/components/chat-list-item/chat-list-item.d.ts.map +1 -0
  100. package/dist/typescript/components/checkbox/checkbox.d.ts +15 -0
  101. package/dist/typescript/components/checkbox/checkbox.d.ts.map +1 -0
  102. package/dist/typescript/components/dialog/dialog.d.ts +34 -0
  103. package/dist/typescript/components/dialog/dialog.d.ts.map +1 -0
  104. package/dist/typescript/components/divider/divider.d.ts +10 -0
  105. package/dist/typescript/components/divider/divider.d.ts.map +1 -0
  106. package/dist/typescript/components/dropdown/dropdown-menu.d.ts +27 -0
  107. package/dist/typescript/components/dropdown/dropdown-menu.d.ts.map +1 -0
  108. package/dist/typescript/components/dropdown/dropdown.d.ts +4 -0
  109. package/dist/typescript/components/dropdown/dropdown.d.ts.map +1 -0
  110. package/dist/typescript/components/dropdown/types.d.ts +52 -0
  111. package/dist/typescript/components/dropdown/types.d.ts.map +1 -0
  112. package/dist/typescript/components/icon-button/icon-button.d.ts +20 -0
  113. package/dist/typescript/components/icon-button/icon-button.d.ts.map +1 -0
  114. package/dist/typescript/components/input/currency-input.d.ts +10 -0
  115. package/dist/typescript/components/input/currency-input.d.ts.map +1 -0
  116. package/dist/typescript/components/input/input.d.ts +32 -0
  117. package/dist/typescript/components/input/input.d.ts.map +1 -0
  118. package/dist/typescript/components/input/otp-input.d.ts +23 -0
  119. package/dist/typescript/components/input/otp-input.d.ts.map +1 -0
  120. package/dist/typescript/components/input/password-input.d.ts +14 -0
  121. package/dist/typescript/components/input/password-input.d.ts.map +1 -0
  122. package/dist/typescript/components/input/phone-input.d.ts +13 -0
  123. package/dist/typescript/components/input/phone-input.d.ts.map +1 -0
  124. package/dist/typescript/components/input/url-input.d.ts +10 -0
  125. package/dist/typescript/components/input/url-input.d.ts.map +1 -0
  126. package/dist/typescript/components/list-item/list-item.d.ts +30 -0
  127. package/dist/typescript/components/list-item/list-item.d.ts.map +1 -0
  128. package/dist/typescript/components/pin-code-field/pin-code-field.d.ts +25 -0
  129. package/dist/typescript/components/pin-code-field/pin-code-field.d.ts.map +1 -0
  130. package/dist/typescript/components/progress/progress.d.ts +18 -0
  131. package/dist/typescript/components/progress/progress.d.ts.map +1 -0
  132. package/dist/typescript/components/radio/radio.d.ts +12 -0
  133. package/dist/typescript/components/radio/radio.d.ts.map +1 -0
  134. package/dist/typescript/components/skeleton/skeleton.d.ts +16 -0
  135. package/dist/typescript/components/skeleton/skeleton.d.ts.map +1 -0
  136. package/dist/typescript/components/switch/switch.d.ts +12 -0
  137. package/dist/typescript/components/switch/switch.d.ts.map +1 -0
  138. package/dist/typescript/components/tabs/tabs.d.ts +37 -0
  139. package/dist/typescript/components/tabs/tabs.d.ts.map +1 -0
  140. package/dist/typescript/components/toast/toast-provider.d.ts +22 -0
  141. package/dist/typescript/components/toast/toast-provider.d.ts.map +1 -0
  142. package/dist/typescript/components/toast/toast.d.ts +15 -0
  143. package/dist/typescript/components/toast/toast.d.ts.map +1 -0
  144. package/dist/typescript/index.d.ts +36 -0
  145. package/dist/typescript/index.d.ts.map +1 -0
  146. package/dist/typescript/package.json +1 -0
  147. package/dist/typescript/theme/design-tokens.generated.d.ts +375 -0
  148. package/dist/typescript/theme/design-tokens.generated.d.ts.map +1 -0
  149. package/dist/typescript/theme/theme-provider.d.ts +8 -0
  150. package/dist/typescript/theme/theme-provider.d.ts.map +1 -0
  151. package/dist/typescript/theme/theme.d.ts +52 -0
  152. package/dist/typescript/theme/theme.d.ts.map +1 -0
  153. package/package.json +88 -0
  154. 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
+ ```