@retray-dev/ui-kit 5.2.0 → 6.0.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 (42) hide show
  1. package/COMPONENTS.md +500 -140
  2. package/EXAMPLES.md +666 -0
  3. package/README.md +3 -3
  4. package/dist/index.d.mts +253 -49
  5. package/dist/index.d.ts +253 -49
  6. package/dist/index.js +955 -610
  7. package/dist/index.mjs +886 -552
  8. package/package.json +9 -3
  9. package/src/components/Accordion/Accordion.tsx +31 -4
  10. package/src/components/AlertBanner/AlertBanner.tsx +16 -33
  11. package/src/components/Avatar/Avatar.tsx +21 -7
  12. package/src/components/Button/Button.tsx +34 -13
  13. package/src/components/ButtonGroup/ButtonGroup.tsx +60 -0
  14. package/src/components/ButtonGroup/index.ts +1 -0
  15. package/src/components/Card/Card.tsx +12 -9
  16. package/src/components/Chip/Chip.tsx +8 -1
  17. package/src/components/ConfirmDialog/ConfirmDialog.tsx +4 -4
  18. package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +38 -5
  19. package/src/components/DetailRow/DetailRow.tsx +140 -0
  20. package/src/components/DetailRow/index.ts +1 -0
  21. package/src/components/EmptyState/EmptyState.tsx +21 -6
  22. package/src/components/Input/Input.tsx +21 -10
  23. package/src/components/LabelValue/LabelValue.tsx +25 -4
  24. package/src/components/ListItem/ListItem.tsx +14 -8
  25. package/src/components/MediaCard/MediaCard.tsx +1 -0
  26. package/src/components/MenuItem/MenuItem.tsx +206 -0
  27. package/src/components/MenuItem/index.ts +2 -0
  28. package/src/components/MonthPicker/MonthPicker.tsx +18 -6
  29. package/src/components/Select/Select.tsx +1 -1
  30. package/src/components/Separator/Separator.tsx +2 -0
  31. package/src/components/Sheet/Sheet.tsx +165 -36
  32. package/src/components/Sheet/index.ts +1 -1
  33. package/src/components/Tabs/Tabs.tsx +4 -4
  34. package/src/components/Textarea/Textarea.tsx +66 -29
  35. package/src/components/Toast/Toast.tsx +41 -267
  36. package/src/components/Toast/index.ts +1 -2
  37. package/src/components/Toggle/Toggle.tsx +2 -2
  38. package/src/index.ts +6 -0
  39. package/src/theme/colors.ts +3 -0
  40. package/src/theme/types.ts +11 -0
  41. package/src/tokens.ts +4 -4
  42. package/src/utils/typography.ts +24 -0
package/COMPONENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- # @retray-dev/ui-kit — Component Reference (v5.2.0)
1
+ # @retray-dev/ui-kit — Component Reference (v6.0.0)
2
2
 
3
3
  This file is the AI reference for this package. It is shipped inside the npm package so consuming projects can import it into their `CLAUDE.md` with:
4
4
 
@@ -36,11 +36,11 @@ export default function App() {
36
36
  ```
37
37
 
38
38
  **Provider order is mandatory:**
39
- - `SafeAreaProvider` must be outermost — required by `useSafeAreaInsets` in `ToastProvider`
39
+ - `SafeAreaProvider` must be outermost — required by `@gorhom/bottom-sheet`
40
40
  - `initialMetrics={initialWindowMetrics}` is required on Android to avoid a "No safe area value available" crash on first render
41
41
  - `GestureHandlerRootView` must wrap everything — required by `@gorhom/bottom-sheet`
42
42
  - `BottomSheetModalProvider` must be inside `GestureHandlerRootView`
43
- - `ToastProvider` must be inside `SafeAreaProvider`
43
+ - `ToastProvider` wraps children and renders `Toaster` (from `sonner-native`) internally
44
44
 
45
45
  ## Typography — Poppins (Required)
46
46
 
@@ -136,6 +136,9 @@ These are the only values you need to supply when customizing the theme. The lib
136
136
  | `successForeground` | `#ffffff` | `#ffffff` | Text/icon on success backgrounds |
137
137
  | `warning` | `#e67e00` | `#f57c00` | Warning / caution states |
138
138
  | `warningForeground` | `#ffffff` | `#ffffff` | Text/icon on warning backgrounds |
139
+ | `overlay` *(optional)* | `rgba(0,0,0,0.45)` | `rgba(0,0,0,0.45)` | Backdrop/overlay color behind sheets and dialogs |
140
+ | `accent` *(optional)* | same as `primary` | same as `primary` | Secondary brand accent (e.g. Airbnb coral). Falls back to `primary` |
141
+ | `accentForeground` *(optional)* | same as `primaryForeground` | same as `primaryForeground` | Text/icon on accent backgrounds. Falls back to `primaryForeground` |
139
142
 
140
143
  ### Derived Tokens (ResolvedColors) — read-only via useTheme().colors
141
144
 
@@ -155,6 +158,9 @@ The full palette components consume. Never supply these directly — they are co
155
158
  | `warningBorder` | `warning` @ 30% | Warning banner border |
156
159
  | `ring` | `= primary` | Focus ring color (always matches primary) |
157
160
  | `input` | `= border` | Input field border (always matches border) |
161
+ | `overlay` | `overlay` token or `rgba(0,0,0,0.45)` | Backdrop behind sheets and dialogs |
162
+ | `accentResolved` | `accent` token or `= primary` | Resolved accent color — always present |
163
+ | `accentForegroundResolved` | `accentForeground` token or `= primaryForeground` | Resolved text on accent — always present |
158
164
 
159
165
  **Usage example — building a custom component using derived tokens:**
160
166
  ```tsx
@@ -231,10 +237,10 @@ import { SPACING, ICON_SIZES, RADIUS, SHADOWS, BREAKPOINTS, TYPOGRAPHY } from '@
231
237
  | `none` | 0 | No rounding |
232
238
  | `xs` | 4 | Micro chips, tags |
233
239
  | `sm` | 8 | Inputs, Textarea, Select, Checkbox |
234
- | `md` | 14 | Cards, MediaCard, AlertBanner, Toast, EmptyState |
240
+ | `md` | 14 | Cards, Buttons (all variants), MediaCard, AlertBanner, Toast, EmptyState |
235
241
  | `lg` | 20 | Sheet top corners |
236
- | `xl` | 32 | Primary CTA buttons (pill-like) |
237
- | `full` | 9999 | Circular elements, CategoryStrip chips |
242
+ | `xl` | 32 | Large decorative elements |
243
+ | `full` | 9999 | IconButton (circle), CategoryStrip chips |
238
244
 
239
245
  **Types:** `Radius`, `RadiusKey`
240
246
 
@@ -286,7 +292,7 @@ All components use these tokens for text styling. Import and use in custom compo
286
292
  | `caption-sm` | 13 | 400 | 16 | 0 | Timestamps, metadata |
287
293
  | `badge-text` | 11 | 600 | 13 | 0 | Badge labels, small tags |
288
294
  | `micro-label` | 12 | 700 | 16 | 0 | Micro labels, overlines |
289
- | `uppercase-tag` | 8 | 700 | 10 | 0.32 | Uppercase decorative tags (auto-uppercase) |
295
+ | `uppercase-tag` | 10 | 700 | 13 | 0.8 | Uppercase decorative tags (auto-uppercase) |
290
296
  | `button-lg` | 16 | 500 | 20 | 0 | Button labels (md/lg size) |
291
297
  | `button-sm` | 14 | 500 | 18 | 0 | Button labels (sm size) |
292
298
 
@@ -344,6 +350,59 @@ const styles = StyleSheet.create({
344
350
 
345
351
  ---
346
352
 
353
+ ## Migration Guide: v5 → v6
354
+
355
+ ### New Components
356
+
357
+ **`MenuItem`** — Navigation row with icon, label, and optional right slot (`rightRender`). Replaces ad-hoc `ListItem` usage for settings/nav menus. Zero horizontal padding by design — consumer controls spacing.
358
+
359
+ ```tsx
360
+ import { MenuItem } from '@retray-dev/ui-kit'
361
+ // variants: 'plain' (default) | 'card'
362
+ <MenuItem label="Profile" iconName="user" onPress={() => {}} />
363
+ <MenuItem label="Notifications" iconName="bell" rightRender={<Switch value={on} onValueChange={setOn} />} showChevron={false} onPress={() => {}} />
364
+ ```
365
+
366
+ ### Breaking Changes
367
+
368
+ **Sheet keyboard defaults changed:**
369
+ | Prop | v5 default | v6 default |
370
+ |------|-----------|-----------|
371
+ | `keyboardBehavior` | `'fillParent'` (Android) / `'interactive'` (iOS) | `'interactive'` (both) |
372
+ | `android_keyboardInputMode` | `'adjustResize'` | `'adjustPan'` |
373
+
374
+ If you relied on `adjustResize`, set it explicitly: `android_keyboardInputMode="adjustResize"`.
375
+
376
+ **Toast API simplified:**
377
+
378
+ ```tsx
379
+ // v5 — hook-only API
380
+ const { toast } = useToast()
381
+ toast.success('Done')
382
+
383
+ // v6 — direct import preferred (hook still works for compat)
384
+ import { toast } from '@retray-dev/ui-kit'
385
+ toast.success('Done')
386
+ ```
387
+
388
+ ### New Theme Tokens
389
+
390
+ Three optional `ThemeColors` tokens — fall back gracefully if omitted:
391
+
392
+ | Token | Default | Purpose |
393
+ |-------|---------|---------|
394
+ | `overlay` | `rgba(0,0,0,0.45)` | Backdrop color behind sheets and dialogs |
395
+ | `accent` | `= primary` | Secondary brand accent color |
396
+ | `accentForeground` | `= primaryForeground` | Text on accent backgrounds |
397
+
398
+ Access resolved values via `useTheme().colors.overlay`, `.accentResolved`, `.accentForegroundResolved`.
399
+
400
+ ### Token Corrections
401
+
402
+ - `TYPOGRAPHY['uppercase-tag']` — size corrected to `10` (was documented as `8`, actual value was always `10`)
403
+
404
+ ---
405
+
347
406
  ## Components
348
407
 
349
408
  ---
@@ -381,7 +440,7 @@ const styles = StyleSheet.create({
381
440
  | `caption-sm` | 13 | 400 | `foregroundMuted` | Timestamps, metadata, helper text |
382
441
  | `badge-text` | 11 | 600 | `foreground` | Badge labels, small status tags |
383
442
  | `micro-label` | 12 | 700 | `foreground` | Micro labels, overlines |
384
- | `uppercase-tag` | 8 | 700 | `foreground` | Decorative uppercase category labels (auto-transforms text) |
443
+ | `uppercase-tag` | 10 | 700 | `foreground` | Decorative uppercase category labels (auto-transforms text) |
385
444
  | `button-lg` | 16 | 500 | `foreground` | Internal use in Button component (md/lg) |
386
445
  | `button-sm` | 14 | 500 | `foreground` | Internal use in Button component (sm) |
387
446
 
@@ -456,7 +515,7 @@ const styles = StyleSheet.create({
456
515
  | `md` | 48px | 24px | 18px | `button-lg` (16pt/500) |
457
516
  | `lg` | 56px | 28px | 20px | `button-lg` (16pt/500) |
458
517
 
459
- **Shape:** `borderRadius: RADIUS.xl = 32px` — pill-shaped on all sizes.
518
+ **Shape:** `borderRadius: RADIUS.md = 14px` — Airbnb-aligned rounded rect on all variants. Exception: `IconButton` uses `RADIUS.full` (perfect circle).
460
519
 
461
520
  **Animations:** Scale springs to 0.95 on `onPressIn`, back to 1.0 on `onPressOut`.
462
521
 
@@ -499,6 +558,67 @@ const styles = StyleSheet.create({
499
558
 
500
559
  ---
501
560
 
561
+ ### ButtonGroup
562
+
563
+ **Import:** `import { ButtonGroup } from '@retray-dev/ui-kit'`
564
+
565
+ **When to use:** Auto-distribute space equally between buttons in horizontal or vertical layouts. Perfect for side-by-side CTAs (Cancel/Confirm, Back/Next) where both buttons should occupy equal width.
566
+
567
+ | Prop | Type | Default | Notes |
568
+ |------|------|---------|-------|
569
+ | children | `React.ReactNode` | required | Button components to lay out |
570
+ | gap | `number` | `12` | Spacing between buttons (in points) |
571
+ | vertical | `boolean` | `false` | Stack buttons vertically instead of horizontally |
572
+ | style | `ViewStyle` | — | Override container style |
573
+
574
+ **Behavior:** Automatically applies `flex: 1` to all direct children. Children share space equally regardless of label length.
575
+
576
+ **Mobile best practice:** When using 2 buttons with icons in a ButtonGroup, use `size="sm"` to prevent text clipping on narrow screens:
577
+ ```tsx
578
+ // ✅ Good — Small size prevents overflow
579
+ <ButtonGroup>
580
+ <Button label="Cancel" variant="secondary" size="sm" iconName="x" onPress={handleCancel} />
581
+ <Button label="Confirm" size="sm" iconName="check" onPress={handleConfirm} />
582
+ </ButtonGroup>
583
+
584
+ // ❌ Avoid — Default size may clip text when icons present
585
+ <ButtonGroup>
586
+ <Button label="Cancel" variant="secondary" iconName="x" onPress={handleCancel} />
587
+ <Button label="Confirm" iconName="check" onPress={handleConfirm} />
588
+ </ButtonGroup>
589
+ ```
590
+
591
+ **Examples:**
592
+ ```tsx
593
+ // Horizontal pair (50%/50%)
594
+ <ButtonGroup>
595
+ <Button label="Cancel" variant="secondary" onPress={handleCancel} />
596
+ <Button label="Confirm" onPress={handleConfirm} />
597
+ </ButtonGroup>
598
+
599
+ // Custom gap
600
+ <ButtonGroup gap={8}>
601
+ <Button label="Back" variant="text" onPress={handleBack} />
602
+ <Button label="Next" onPress={handleNext} />
603
+ </ButtonGroup>
604
+
605
+ // Vertical stack
606
+ <ButtonGroup vertical gap={8}>
607
+ <Button label="Primary Action" onPress={handlePrimary} />
608
+ <Button label="Secondary Action" variant="secondary" onPress={handleSecondary} />
609
+ <Button label="Delete" variant="destructive" onPress={handleDelete} />
610
+ </ButtonGroup>
611
+
612
+ // Three buttons (33%/33%/33%)
613
+ <ButtonGroup gap={6}>
614
+ <Button label="1D" variant="secondary" size="sm" onPress={() => setRange('1d')} />
615
+ <Button label="1W" variant="secondary" size="sm" onPress={() => setRange('1w')} />
616
+ <Button label="1M" size="sm" onPress={() => setRange('1m')} />
617
+ </ButtonGroup>
618
+ ```
619
+
620
+ ---
621
+
502
622
  ### IconButton
503
623
 
504
624
  **Import:** `import { IconButton } from '@retray-dev/ui-kit'`
@@ -682,6 +802,9 @@ const styles = StyleSheet.create({
682
802
  | error | `string` | — | Error text below; turns border destructive (2px) |
683
803
  | hint | `string` | — | Helper text below (hidden when `error` is set) |
684
804
  | rows | `number` | `4` | Minimum row count — each row ≈ 30px. Sets `numberOfLines` |
805
+ | prefixIcon | `string` | — | Icon name from `@expo/vector-icons` rendered inside top-left corner |
806
+ | prefixIconNode | `ReactNode` | — | Custom icon node rendered top-left |
807
+ | prefixIconColor | `string` | — | Override prefix icon color. Defaults to `foregroundMuted` |
685
808
  | containerStyle | `ViewStyle` | — | Outer container style |
686
809
  | style | `TextStyle` | — | TextInput element style |
687
810
 
@@ -689,9 +812,9 @@ const styles = StyleSheet.create({
689
812
 
690
813
  **Examples:**
691
814
  ```tsx
692
- <Textarea label="Bio" placeholder="Tell us about yourself" rows={5} />
693
- <Textarea label="Review" hint="Be honest and helpful" />
694
- <Textarea label="Notes" error="Notes are required" rows={3} />
815
+ <Textarea label="Bio" placeholder="Tell us about yourself" rows={5} prefixIcon="user" />
816
+ <Textarea label="Review" hint="Be honest and helpful" prefixIcon="message-circle" />
817
+ <Textarea label="Notes" error="Notes are required" rows={3} prefixIcon="edit-3" />
695
818
  ```
696
819
 
697
820
  ---
@@ -776,6 +899,9 @@ const [amount, setAmount] = useState(0)
776
899
  | prefix | `string` | `'$'` | Symbol prepended to formatted value |
777
900
  | showDecimals | `boolean` | `false` | Show two decimal places with comma separator (e.g. `$25.000,00`) |
778
901
  | textColor | `string` | — | Override text color. Defaults to `foreground` token |
902
+ | variant | `'hero' \| 'large' \| 'medium' \| 'small'` | — | Predefined size (48/32/18/14pt). Overrides default 56pt |
903
+ | autoScale | `boolean` | `false` | Enable `adjustsFontSizeToFit` — long values shrink to fit one line |
904
+ | maxFontSize | `number` | — | Max font size when `autoScale` true. Defaults to variant size or 56pt |
779
905
  | style | `ViewStyle` | — | Outer container style |
780
906
 
781
907
  **Format:** Dot (`.`) as thousands separator, comma (`,`) as decimal separator — Latin American / European format.
@@ -874,9 +1000,10 @@ const [amount, setAmount] = useState(0)
874
1000
 
875
1001
  | Prop | Type | Default | Notes |
876
1002
  |------|------|---------|-------|
877
- | src | `string` | — | Image URI (remote or local) |
878
- | fallback | `string` | — | Text shown when image fails or `src` is absent — first 2 characters shown, uppercased |
879
- | size | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Diameter in points |
1003
+ | src | `string \| null` | — | Image URI (remote or local). `null` forces fallback |
1004
+ | fallback | `string` | — | Manual initials (max 2 chars, uppercased) |
1005
+ | fallbackText | `string` | | Full name extracts up to 2 initials (e.g. `"Julian Cruz"` `"JC"`) |
1006
+ | size | `'sm' \| 'md' \| 'lg' \| 'xl' \| number` | `'md'` | Named size or custom diameter in points |
880
1007
  | status | `'online' \| 'offline' \| 'busy' \| 'away'` | — | Status indicator dot, bottom-right |
881
1008
  | style | `ViewStyle` | — | — |
882
1009
 
@@ -903,6 +1030,8 @@ const [amount, setAmount] = useState(0)
903
1030
  <Avatar fallback="AN" size="md" />
904
1031
  <Avatar src={user.avatar} fallback={user.initials} size="xl" status="online" />
905
1032
  <Avatar fallback="BU" size="sm" status="busy" />
1033
+ <Avatar fallbackText="Julian Cruz" size="md" /> // → "JC"
1034
+ <Avatar src={avatarUrl} size={64} /> // custom numeric size
906
1035
 
907
1036
  // In a ListItem
908
1037
  <ListItem
@@ -1773,7 +1902,14 @@ const [tab, setTab] = useState('profile')
1773
1902
 
1774
1903
  **AccordionItem type:**
1775
1904
  ```ts
1776
- { value: string; trigger: string; content: ReactNode }
1905
+ {
1906
+ value: string
1907
+ trigger: string
1908
+ content: ReactNode
1909
+ iconName?: string // Icon name from @expo/vector-icons
1910
+ icon?: ReactNode // Custom icon node
1911
+ iconColor?: string // Override icon color (defaults to foregroundMuted)
1912
+ }
1777
1913
  ```
1778
1914
 
1779
1915
  **Animation:** `react-native-reanimated` `withTiming` (220ms) for height and chevron rotation (180°). Press scale uses `withSpring`. Runs on UI thread at 60fps.
@@ -1787,11 +1923,13 @@ const [tab, setTab] = useState('profile')
1787
1923
  {
1788
1924
  value: 'q1',
1789
1925
  trigger: 'What payment methods do you accept?',
1926
+ iconName: 'credit-card',
1790
1927
  content: <Text variant="body-sm">We accept Visa, Mastercard, and bank transfers.</Text>,
1791
1928
  },
1792
1929
  {
1793
1930
  value: 'q2',
1794
1931
  trigger: 'Can I cancel my booking?',
1932
+ iconName: 'x-circle',
1795
1933
  content: <Text variant="body-sm">Yes — free cancellation within 48 hours of booking.</Text>,
1796
1934
  },
1797
1935
  ]}
@@ -1808,7 +1946,7 @@ const [tab, setTab] = useState('profile')
1808
1946
 
1809
1947
  ### Sheet
1810
1948
 
1811
- **Import:** `import { Sheet, BottomSheetModalProvider } from '@retray-dev/ui-kit'`
1949
+ **Import:** `import { Sheet, SheetTextInput, BottomSheetModalProvider } from '@retray-dev/ui-kit'`
1812
1950
 
1813
1951
  **When to use:** Bottom sheet for contextual actions, filters, pickers, or detail views that don't need a full screen. Auto-sizes to content — no snap points needed.
1814
1952
 
@@ -1825,158 +1963,156 @@ Add `react-native-worklets/plugin` (not `react-native-reanimated/plugin`) to `ba
1825
1963
  | open | `boolean` | required | `true` presents the sheet, `false` dismisses it |
1826
1964
  | onClose | `() => void` | required | Called on swipe-dismiss or backdrop press |
1827
1965
  | title | `string` | — | Sheet heading |
1828
- | description | `string` | — | Supporting text below title |
1966
+ | subtitle | `string` | — | Supporting text below title (replaces deprecated `description`) |
1967
+ | showCloseButton | `boolean` | `false` | Show X close button in the header |
1829
1968
  | children | `ReactNode` | — | Sheet content |
1830
- | style | `ViewStyle` | — | Inner content container style |
1831
- | scrollable | `boolean` | `false` | Wraps children in `BottomSheetScrollView` — fixes gesture conflict on both platforms when content needs to scroll |
1832
- | maxHeight | `number` | | Caps sheet height (dp). Automatically enables scrolling when content exceeds this value |
1969
+ | style | `ViewStyle` | — | Inner scroll/content container style |
1970
+ | contentStyle | `ViewStyle` | | Outer content wrapper style |
1971
+ | scrollable | `boolean` | `false` | Wraps children in `BottomSheetScrollView` |
1972
+ | maxHeight | `number` | `~85% screen` | Caps sheet height (dp). Defaults to 85% of screen height. Enables scrolling when content overflows |
1973
+ | keyboardBehavior | `'extend' \| 'fillParent' \| 'interactive'` | `'interactive'` | How sheet responds to keyboard. `'interactive'` (default) offsets sheet by keyboard size — works on both platforms. `'fillParent'` extends to fill parent view. `'extend'` extends to max snap point |
1974
+ | keyboardBlurBehavior | `'none' \| 'restore'` | `'restore'` | What happens when keyboard dismisses: `'restore'` → return to pre-keyboard position (recommended), `'none'` → stay at keyboard-adjusted position |
1975
+ | enableBlurKeyboardOnGesture | `boolean` | `true` | Dismiss keyboard when user starts dragging sheet down (recommended for better UX) |
1976
+ | android_keyboardInputMode | `'adjustPan' \| 'adjustResize'` | `'adjustPan'` | Android-only: `'adjustPan'` moves the window (default — fixes restore issues with dynamic sizing). `'adjustResize'` resizes the container (can cause transparent gap when keyboard dismisses) |
1977
+ | footer | `ReactNode` | — | Sticky footer below scroll area (sticky above keyboard) |
1978
+ | snapPoints | `(string \| number)[]` | — | Optional snap points (e.g., `['50%', '85%']`). When omitted, uses dynamic sizing (auto-fits content) |
1833
1979
 
1834
1980
  **Features:**
1835
- - `enableDynamicSizing` — height auto-fits content, no `snapPoints` needed
1981
+ - `enableDynamicSizing` — height auto-fits content, no `snapPoints` needed (default behavior when `snapPoints` is omitted)
1982
+ - `snapPoints` — optionally provide custom snap points (e.g., `['50%', '85%']`). Disables dynamic sizing when provided
1836
1983
  - `enablePanDownToClose` — swipe down to dismiss
1837
1984
  - Backdrop press dismisses
1838
- - **Scrollable content:** use `scrollable` prop (explicit) or `maxHeight` prop (capped height). Both use `BottomSheetScrollView` internally — do NOT use plain `ScrollView` inside Sheet, it breaks gesture handling on iOS
1839
-
1840
- **Styling:** `RADIUS.lg = 20px` top corners, 16px horizontal + 20px bottom padding.
1841
-
1842
- **Haptics:** `impactLight` on open.
1985
+ - **Scrollable content:** use `scrollable` prop or `maxHeight`. Both use `BottomSheetScrollView` — do NOT use plain `ScrollView` inside Sheet
1986
+ - **Keyboard handling:** Full keyboard awareness via `@gorhom/bottom-sheet` v5. Professional defaults:
1987
+ - `keyboardBehavior="interactive"` sheet offsets by keyboard size on both platforms
1988
+ - `android_keyboardInputMode="adjustPan"` — moves window instead of resizing (fixes restore issues on Android)
1989
+ - `keyboardBlurBehavior="restore"` returns to pre-keyboard position when keyboard dismisses
1990
+ - `enableBlurKeyboardOnGesture={true}` — dismisses keyboard when dragging sheet down
1991
+ - `topInset` — automatically applied from safe area context to prevent going above notch
1992
+ - **Text inputs inside sheet:** **MUST use `SheetTextInput`** (re-exported `BottomSheetTextInput`) — handles focus/blur internally, communicates with sheet's keyboard system. **Never use regular `TextInput`** inside sheets — keyboard handling will break
1993
+ - **Custom TextInput:** If using custom input components, you must copy `handleOnFocus`/`handleOnBlur` from [BottomSheetTextInput source](https://github.com/gorhom/react-native-bottom-sheet/blob/master/src/components/bottomSheetTextInput/BottomSheetTextInput.tsx)
1994
+
1995
+ **Haptics:** `impactMedium` on open.
1843
1996
 
1844
1997
  **Examples:**
1845
1998
  ```tsx
1846
1999
  const [open, setOpen] = useState(false)
1847
2000
 
2001
+ // Basic
1848
2002
  <Sheet open={open} onClose={() => setOpen(false)} title="Sort by">
1849
- <RadioGroup
1850
- options={[
1851
- { label: 'Newest', value: 'newest' },
1852
- { label: 'Price: Low to High', value: 'price_asc' },
1853
- { label: 'Price: High to Low', value: 'price_desc' },
1854
- { label: 'Rating', value: 'rating' },
1855
- ]}
1856
- value={sort}
1857
- onValueChange={(v) => { setSort(v); setOpen(false) }}
2003
+ <RadioGroup options={sortOptions} value={sort} onValueChange={setSort} />
2004
+ </Sheet>
2005
+
2006
+ // Scrollable list
2007
+ <Sheet open={open} onClose={() => setOpen(false)} title="Results" scrollable maxHeight={500}>
2008
+ {items.map((r) => <ListItem key={r.id} title={r.name} showSeparator />)}
2009
+ </Sheet>
2010
+
2011
+ // With text input — keyboard handling works automatically (platform-optimized defaults)
2012
+ <Sheet
2013
+ open={open}
2014
+ onClose={() => setOpen(false)}
2015
+ title="Add note"
2016
+ subtitle="Keyboard handling is automatic with platform-optimized defaults"
2017
+ >
2018
+ <SheetTextInput
2019
+ placeholder="Write your note..."
2020
+ multiline
2021
+ style={{
2022
+ borderWidth: 1,
2023
+ borderColor: '#ddd',
2024
+ borderRadius: 8,
2025
+ padding: 12,
2026
+ minHeight: 80,
2027
+ }}
1858
2028
  />
2029
+ <Button label="Save" fullWidth style={{ marginTop: 12 }} onPress={() => setOpen(false)} />
1859
2030
  </Sheet>
1860
2031
 
1861
- // Scrollable sheet long list
1862
- <Sheet open={open} onClose={() => setOpen(false)} title="Logs" scrollable>
1863
- {logs.map((log) => (
1864
- <Text key={log.id}>{log.message}</Text>
1865
- ))}
2032
+ // With custom snap points (manual sizing)
2033
+ <Sheet
2034
+ open={open}
2035
+ onClose={() => setOpen(false)}
2036
+ title="Filters"
2037
+ snapPoints={['50%', '90%']}
2038
+ >
2039
+ <RadioGroup options={filterOptions} value={filter} onValueChange={setFilter} />
1866
2040
  </Sheet>
1867
2041
 
1868
- // Capped height — scroll kicks in when content overflows 400dp
1869
- <Sheet open={open} onClose={() => setOpen(false)} title="Results" maxHeight={400}>
1870
- {results.map((r) => <ListItem key={r.id} title={r.name} />)}
2042
+ // With sticky footer
2043
+ <Sheet open={open} onClose={() => setOpen(false)} title="Filters" scrollable
2044
+ footer={<View style={{ padding: 16 }}><Button label="Apply" fullWidth onPress={() => setOpen(false)} /></View>}>
2045
+ {/* long filter content */}
1871
2046
  </Sheet>
1872
2047
 
1873
- // Filter sheet
1874
- <Sheet open={filterOpen} onClose={() => setFilterOpen(false)} title="Filters">
1875
- <View style={{ gap: SPACING.lg }}>
1876
- <View>
1877
- <Text variant="title-sm">Price range</Text>
1878
- <Slider value={maxPrice} minimumValue={0} maximumValue={1000} step={50} onValueChange={setMaxPrice} showValue formatValue={(v) => `$${v}`} />
1879
- </View>
1880
- <Separator />
1881
- <View>
1882
- <Text variant="title-sm">Category</Text>
1883
- <ChipGroup options={categoryOptions} value={selectedCategory} onValueChange={setSelectedCategory} />
1884
- </View>
1885
- <Button label="Apply filters" fullWidth onPress={() => { applyFilters(); setFilterOpen(false) }} />
1886
- </View>
2048
+ // With close button
2049
+ <Sheet open={open} onClose={() => setOpen(false)} title="Details" subtitle="Product info" showCloseButton>
2050
+ <Text>Content here...</Text>
1887
2051
  </Sheet>
1888
2052
  ```
1889
2053
 
2054
+ **Keyboard handling notes:**
2055
+ - No `KeyboardAvoidingView` needed — `@gorhom/bottom-sheet` handles everything
2056
+ - Always use `SheetTextInput` (not plain `TextInput`) for auto-focus/blur handling
2057
+ - Default `keyboardBehavior="interactive"` works on both platforms
2058
+ - Default `android_keyboardInputMode="adjustPan"` fixes the transparent gap that occurs with `adjustResize` when keyboard dismisses
2059
+ - `enableBlurKeyboardOnGesture={true}` (default) dismisses keyboard when dragging sheet
2060
+
1890
2061
  ---
1891
2062
 
1892
2063
  ### Toast / useToast
1893
2064
 
1894
- **Import:** `import { ToastProvider, useToast } from '@retray-dev/ui-kit'`
2065
+ **Import:** `import { toast, ToastProvider, useToast } from '@retray-dev/ui-kit'`
1895
2066
 
1896
2067
  **When to use:** Ephemeral feedback messages — save confirmations, errors, copy notifications, background process updates. Auto-dismiss after duration.
1897
2068
 
1898
- **Required setup:** `ToastProvider` must wrap app inside `SafeAreaProvider`. See Setup section above.
2069
+ **Powered by:** `sonner-native` white/black background, colored icon marks the variant (not the background), swipe-to-dismiss, promise support, stacking.
2070
+
2071
+ **Required setup:** `ToastProvider` must wrap the app. See Setup section above. Also requires `react-native-svg` and `react-native-screens` as peer deps.
1899
2072
 
1900
- **Usage:**
2073
+ **API — direct function (preferred):**
1901
2074
  ```tsx
1902
- function MyComponent() {
1903
- const { toast, dismiss } = useToast()
2075
+ import { toast } from '@retray-dev/ui-kit'
2076
+
2077
+ // Variants:
2078
+ toast.success('Saved successfully')
2079
+ toast.error('Connection error', { description: 'Check your network' })
2080
+ toast.warning('Slow connection detected')
2081
+ toast('Neutral message')
2082
+ toast.loading('Processing...')
2083
+
2084
+ // Promise — auto-resolves:
2085
+ toast.promise(saveData(), {
2086
+ loading: 'Saving...',
2087
+ success: 'Saved',
2088
+ error: 'Failed to save',
2089
+ })
1904
2090
 
1905
- return (
1906
- <Button
1907
- label="Save"
1908
- onPress={async () => {
1909
- await save()
1910
- toast({ title: 'Saved', variant: 'success' })
1911
- }}
1912
- />
1913
- )
1914
- }
2091
+ // Programmatic dismiss:
2092
+ const id = toast.loading('Uploading...')
2093
+ await upload()
2094
+ toast.dismiss(id)
2095
+ toast.success('Upload complete')
1915
2096
  ```
1916
2097
 
1917
- **`toast()` options:**
1918
-
1919
- | Field | Type | Default | Notes |
1920
- |-------|------|---------|-------|
1921
- | title | `string` | — | Bold heading |
1922
- | description | `string` | — | Detail text below title |
1923
- | variant | `'default' \| 'destructive' \| 'success' \| 'warning'` | `'default'` | Background and icon color |
1924
- | duration | `number` (ms) | `3000` | Auto-dismiss delay. Pass `Infinity` to prevent auto-dismiss |
1925
- | icon | `ReactNode` | — | Custom icon. Defaults to variant symbol |
1926
- | iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon` |
1927
- | iconColor | `string` | — | Override icon color |
1928
- | action | `{ label: string, onPress: () => void }` | — | Optional action button beside dismiss |
1929
-
1930
- **`dismiss(id)`:** Dismiss a specific toast programmatically. `id` is returned by the `toast()` call.
1931
-
1932
- **Variant details:**
1933
- - `default` — dark/primary background, `impactLight` haptic
1934
- - `success` — `success` token background, `notificationSuccess` haptic
1935
- - `destructive` — `destructive` token background, `notificationError` haptic
1936
- - `warning` — `warning` token background, `notificationError` haptic
1937
-
1938
- **Behavior:**
1939
- - Max 3 toasts shown simultaneously — oldest removed when 4th arrives
1940
- - Appear at top of screen below status bar (dynamic safe area inset)
1941
- - Swipe left or right to dismiss early (threshold: 80px or 800pt/s velocity)
1942
- - **Web:** 400px max width, centered
1943
-
1944
- **Animation:** Entrance: `withTiming(120ms, Easing.out(Easing.exp))` slide-down + opacity. Exit: `withTiming(200ms)` slide-up + opacity fade.
1945
-
1946
- **Examples:**
2098
+ **API — hook (backward compat):**
1947
2099
  ```tsx
1948
2100
  const { toast, dismiss } = useToast()
1949
-
1950
- // Basic variants
1951
- toast({ title: 'Saved', variant: 'success' })
1952
- toast({ title: 'Connection error', variant: 'destructive' })
1953
- toast({ title: 'Check your email', variant: 'warning' })
1954
- toast({ title: 'Link copied' })
1955
-
1956
- // With description
1957
- toast({
1958
- title: 'Payment sent',
1959
- description: '$250 sent to John Doe',
1960
- variant: 'success',
1961
- iconName: 'check-circle',
1962
- })
1963
-
1964
- // With action button
1965
- toast({
1966
- title: 'Message deleted',
1967
- action: { label: 'Undo', onPress: () => restoreMessage() },
1968
- })
1969
-
1970
- // Custom duration (longer)
1971
- toast({ title: 'Processing your request...', duration: 8000 })
1972
-
1973
- // Programmatic dismiss
1974
- const id = toast({ title: 'Uploading...', duration: Infinity })
1975
- await upload()
2101
+ toast.success('Saved')
1976
2102
  dismiss(id)
1977
- toast({ title: 'Upload complete', variant: 'success' })
1978
2103
  ```
1979
2104
 
2105
+ **Behavior:**
2106
+ - Max 3 toasts shown simultaneously
2107
+ - Appear top-center with safe area insets (respects notch/status bar)
2108
+ - Swipe up to dismiss early
2109
+ - Auto-dismiss after 4000ms
2110
+ - Follows `colorScheme` from `ThemeProvider` (light/dark background)
2111
+
2112
+ **Notes:**
2113
+ - `richColors: false` — background stays white (light) / black (dark). Variant shown via icon color only.
2114
+ - `closeButton: false` — swipe-to-dismiss is the gesture.
2115
+
1980
2116
  ---
1981
2117
 
1982
2118
  ### ConfirmDialog
@@ -2049,6 +2185,8 @@ toast({ title: 'Upload complete', variant: 'success' })
2049
2185
 
2050
2186
  **When to use:** Rows in lists, feeds, menus, settings screens. The most compositional component — composes left slot, text block, and right slot in a standard horizontal layout. Supports plain (list items) and card (standalone cards) variants.
2051
2187
 
2188
+ **Important:** ListItem has **zero horizontal padding** by design — the consumer controls spacing. Wrap in a container with padding for standalone use, or use `variant="card"` for auto-styled surfaces.
2189
+
2052
2190
  | Prop | Type | Default | Notes |
2053
2191
  |------|------|---------|-------|
2054
2192
  | title | `string` | required | Primary text |
@@ -2159,6 +2297,84 @@ toast({ title: 'Upload complete', variant: 'success' })
2159
2297
 
2160
2298
  ---
2161
2299
 
2300
+ ### MenuItem
2301
+
2302
+ **Import:** `import { MenuItem } from '@retray-dev/ui-kit'`
2303
+
2304
+ **When to use:** Settings screens, navigation lists, menu entries — icon + label + optional right content. Purpose-built for Airbnb-style settings rows. Use `ListItem` when you need subtitle, caption, or more complex layouts.
2305
+
2306
+ **Important:** MenuItem has **zero horizontal padding** by design — the consumer controls spacing. Wrap in a container with padding for standalone use, or use `variant="card"` for auto-styled surfaces.
2307
+
2308
+ | Prop | Type | Default | Notes |
2309
+ |------|------|---------|-------|
2310
+ | label | `string` | required | Row label |
2311
+ | iconName | `string` | — | Icon from `@expo/vector-icons`. Auto-resolved across all 6 families |
2312
+ | icon | `ReactNode` | — | Custom icon (used if `iconName` not set) |
2313
+ | iconColor | `string` | — | Override icon color. Defaults to `foreground` |
2314
+ | rightRender | `ReactNode` | — | Custom content on the right. Replaces default chevron. Use for Switch, Checkbox, Badge, etc. |
2315
+ | showChevron | `boolean` | `true` | Show chevron on the right. Ignored when `rightRender` is set |
2316
+ | onPress | `() => void` | required | Press handler |
2317
+ | variant | `'plain' \| 'card'` | `'plain'` | `plain` sits inside parent surface. `card` has own background + border |
2318
+ | disabled | `boolean` | `false` | Dims and disables press |
2319
+ | showSeparator | `boolean` | `false` | Hairline separator at bottom |
2320
+ | style | `ViewStyle` | — | Container style override |
2321
+ | labelStyle | `TextStyle` | — | Label text style override |
2322
+
2323
+ **Features:**
2324
+ - Height: 54dp — consistent settings row
2325
+ - Chevron: shown by default (`showChevron={true}`). Can be replaced via `rightRender` prop
2326
+ - `rightRender` — arbitrary right slot content (Switch, Checkbox, Badge, etc). When set, chevron is hidden
2327
+ - Press scale: 0.97 spring (`stiffness: 350, damping: 28, mass: 0.9`)
2328
+ - Haptics: `selectionAsync` on press
2329
+
2330
+ **Example:**
2331
+ ```tsx
2332
+ // Settings screen (wrap in container with padding)
2333
+ <View style={{ padding: 16, backgroundColor: colors.card, borderRadius: 12 }}>
2334
+ <MenuItem label="Personal info" iconName="user" onPress={() => navigate('profile')} showSeparator />
2335
+ <MenuItem label="Security" iconName="shield" onPress={() => navigate('security')} showSeparator />
2336
+ <MenuItem label="Privacy" iconName="lock" onPress={() => navigate('privacy')} showSeparator />
2337
+ <MenuItem
2338
+ label="Notifications"
2339
+ iconName="bell"
2340
+ onPress={() => setNotifs(!notifs)}
2341
+ rightRender={<Switch value={notifs} onValueChange={setNotifs} />}
2342
+ showSeparator
2343
+ />
2344
+ <MenuItem
2345
+ label="Dark Mode"
2346
+ iconName="moon"
2347
+ onPress={() => setDark(!dark)}
2348
+ rightRender={<Checkbox checked={dark} onPress={() => setDark(!dark)} />}
2349
+ showSeparator
2350
+ />
2351
+ <MenuItem
2352
+ label="Payments"
2353
+ iconName="credit-card"
2354
+ onPress={() => navigate('payments')}
2355
+ rightRender={<Badge label="New" variant="warning" size="sm" />}
2356
+ />
2357
+ </View>
2358
+
2359
+ // Card variant (standalone)
2360
+ <MenuItem
2361
+ variant="card"
2362
+ label="Refer a friend"
2363
+ iconName="gift"
2364
+ onPress={() => navigate('referral')}
2365
+ />
2366
+
2367
+ // No chevron (custom right content)
2368
+ <MenuItem
2369
+ label="Language"
2370
+ iconName="globe"
2371
+ rightRender={<Text>English</Text>}
2372
+ onPress={() => navigate('language')}
2373
+ />
2374
+ ```
2375
+
2376
+ ---
2377
+
2162
2378
  ### Chip / ChipGroup
2163
2379
 
2164
2380
  **Import:** `import { Chip, ChipGroup } from '@retray-dev/ui-kit'`
@@ -2180,7 +2396,7 @@ toast({ title: 'Upload complete', variant: 'success' })
2180
2396
 
2181
2397
  | Prop | Type | Default | Notes |
2182
2398
  |------|------|---------|-------|
2183
- | options | `ChipOption[]` | required | `{ label: string, value: string \| number }` |
2399
+ | options | `ChipOption[]` | required | `{ label, value, iconName?, iconColor?, disabled? }` |
2184
2400
  | value | `string \| number \| (string \| number)[]` | — | Selected value(s) |
2185
2401
  | onValueChange | `(value: ...) => void` | — | Returns single value or array depending on `multiSelect` |
2186
2402
  | multiSelect | `boolean` | `false` | Allow multiple chips selected simultaneously |
@@ -2325,29 +2541,98 @@ const [categories, setCategories] = useState<number[]>([1, 3])
2325
2541
  |------|------|---------|-------|
2326
2542
  | label | `string` | required | Caption label on the left |
2327
2543
  | value | `string \| ReactNode` | required | Value on the right. Strings auto-styled; pass `ReactNode` for custom content |
2544
+ | iconName | `string` | — | Icon name from `@expo/vector-icons` rendered left of label |
2545
+ | iconColor | `string` | — | Override icon color. Defaults to `foregroundMuted` |
2328
2546
  | style | `ViewStyle` | — | — |
2329
2547
 
2330
2548
  **Styling:** Row layout, `justifyContent: 'space-between'`. Label: 13pt / Regular / `foregroundMuted`. Value: 14pt / Medium / `foreground`. 12px gap.
2331
2549
 
2332
2550
  **Examples:**
2333
2551
  ```tsx
2334
- <LabelValue label="Date" value="12 mar 2025" />
2335
- <LabelValue label="Category" value="Food & drink" />
2336
- <LabelValue label="Status" value={<Badge label="Pending" variant="warningOutline" size="sm" />} />
2337
- <LabelValue label="Amount" value="$45.000" />
2552
+ <LabelValue label="Date" value="12 mar 2025" iconName="calendar" />
2553
+ <LabelValue label="Category" value="Food & drink" iconName="tag" />
2554
+ <LabelValue label="Status" value={<Badge label="Pending" variant="warningOutline" size="sm" />} iconName="clock" />
2555
+ <LabelValue label="Amount" value="$45.000" iconName="dollar-sign" />
2338
2556
 
2339
2557
  // Transaction detail
2340
2558
  <View style={{ gap: SPACING.sm }}>
2341
- <LabelValue label="Merchant" value={transaction.merchant} />
2342
- <LabelValue label="Date" value={formatDate(transaction.date)} />
2343
- <LabelValue label="Category" value={transaction.category} />
2559
+ <LabelValue label="Merchant" value={transaction.merchant} iconName="shopping-bag" />
2560
+ <LabelValue label="Date" value={formatDate(transaction.date)} iconName="calendar" />
2561
+ <LabelValue label="Category" value={transaction.category} iconName="tag" />
2344
2562
  <Separator />
2345
- <LabelValue label="Amount" value={`$${transaction.amount}`} />
2563
+ <LabelValue label="Amount" value={`$${transaction.amount}`} iconName="dollar-sign" />
2346
2564
  </View>
2347
2565
  ```
2348
2566
 
2349
2567
  ---
2350
2568
 
2569
+ ### DetailRow
2570
+
2571
+ **Import:** `import { DetailRow } from '@retray-dev/ui-kit'`
2572
+
2573
+ **When to use:** "Label ··· Value" ticket/receipt rows — financial summaries, split bills, transaction breakdowns. Dotted separator fills the space between label and value.
2574
+
2575
+ | Prop | Type | Default | Notes |
2576
+ |------|------|---------|-------|
2577
+ | label | `string \| ReactNode` | required | Left side. Strings auto-styled in `foregroundMuted` caption |
2578
+ | value | `string` | required | Right side value text |
2579
+ | separator | `'dotted' \| 'solid' \| 'dashed' \| 'none'` | `'dotted'` | Fill line between label and value |
2580
+ | labelWeight | `'normal' \| 'medium' \| 'semibold' \| 'bold'` | `'normal'` | Font weight of label text |
2581
+ | valueColor | `string` | — | Override value text color (hex or theme token value) |
2582
+ | leftIcon | `ReactNode` | — | Node rendered left of label (e.g. `Avatar`, `Icon`) |
2583
+ | leftIconName | `string` | — | Icon name from `@expo/vector-icons` rendered left of label. Takes precedence over `leftIcon` |
2584
+ | leftIconColor | `string` | — | Override left icon color. Defaults to `foregroundMuted` |
2585
+ | rightIconName | `string` | — | Icon name from `@expo/vector-icons` rendered right of value |
2586
+ | rightIconColor | `string` | — | Override right icon color. Defaults to `foregroundMuted` |
2587
+ | style | `ViewStyle` | — | Row container style |
2588
+ | labelStyle | `TextStyle` | — | Label text style override |
2589
+ | valueStyle | `TextStyle` | — | Value text style override |
2590
+
2591
+ **Examples:**
2592
+ ```tsx
2593
+ // Basic
2594
+ <DetailRow label="Total" value="$50.000" />
2595
+
2596
+ // With icons
2597
+ <DetailRow label="Food" value="$380.000" leftIconName="coffee" />
2598
+ <DetailRow label="Transport" value="$95.000" leftIconName="truck" />
2599
+ <DetailRow label="Total" value="$1.250.000" labelWeight="bold" rightIconName="trending-up" />
2600
+
2601
+ // Dotted separator with bold total
2602
+ <DetailRow label="Total" value="$50.000" labelWeight="bold" />
2603
+
2604
+ // With avatar left icon
2605
+ <DetailRow
2606
+ label="Juliana"
2607
+ value="$25.000"
2608
+ leftIcon={<Avatar src={avatarUrl} size={20} />}
2609
+ />
2610
+
2611
+ // Destructive value
2612
+ <DetailRow label="Deuda" value="$150.000" valueColor={colors.destructive} />
2613
+
2614
+ // No separator
2615
+ <DetailRow label="Subtotal" value="$45.000" separator="none" />
2616
+
2617
+ // Full receipt block
2618
+ <View style={{ gap: SPACING.xs }}>
2619
+ <DetailRow label="Juliana" value="$25.000" leftIcon={<Avatar fallback="J" size={20} />} />
2620
+ <DetailRow label="Julián" value="$25.000" leftIcon={<Avatar fallback="JC" size={20} />} />
2621
+ <Separator />
2622
+ <DetailRow label="Total" value="$50.000" labelWeight="bold" />
2623
+ </View>
2624
+ ```
2625
+
2626
+ **Output:**
2627
+ ```
2628
+ Juliana ············ $25.000
2629
+ Julián ············· $25.000
2630
+ ─────────────────────────────
2631
+ Total ·············· $50.000
2632
+ ```
2633
+
2634
+ ---
2635
+
2351
2636
  ### MonthPicker
2352
2637
 
2353
2638
  **Import:** `import { MonthPicker } from '@retray-dev/ui-kit'`
@@ -2358,6 +2643,8 @@ const [categories, setCategories] = useState<number[]>([1, 3])
2358
2643
  |------|------|---------|-------|
2359
2644
  | value | `MonthPickerValue` | required | `{ month: 1–12, year: number }` |
2360
2645
  | onChange | `(value: MonthPickerValue) => void` | required | Called on navigation |
2646
+ | locale | `string` | `'en'` | BCP 47 locale. Built-in: `'en'`, `'es'`, `'pt'`, `'fr'`. Other locales: use `formatLabel` |
2647
+ | formatLabel | `(value: MonthPickerValue) => string` | — | Custom label formatter. Takes precedence over `locale` |
2361
2648
  | style | `ViewStyle` | — | — |
2362
2649
 
2363
2650
  **MonthPickerValue type:**
@@ -2381,6 +2668,16 @@ const [period, setPeriod] = useState({
2381
2668
  <MonthPicker value={period} onChange={setPeriod} />
2382
2669
  // Displays: "May 2026"
2383
2670
 
2671
+ <MonthPicker value={period} onChange={setPeriod} locale="es" />
2672
+ // Displays: "mayo 2026"
2673
+
2674
+ <MonthPicker
2675
+ value={period}
2676
+ onChange={setPeriod}
2677
+ formatLabel={({ month, year }) => `${month}/${year}`}
2678
+ />
2679
+ // Displays: "5/2026"
2680
+
2384
2681
  // In a transactions header
2385
2682
  <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
2386
2683
  <Text variant="display-md">Transactions</Text>
@@ -2528,6 +2825,43 @@ All components with icon slots accept `iconName` — auto-resolved size and colo
2528
2825
 
2529
2826
  ---
2530
2827
 
2828
+ ### `getResponsiveFontSize` utility
2829
+
2830
+ Calculates a stepped-down font size for long numeric strings (currency amounts, large numbers) that would overflow at full size.
2831
+
2832
+ ```tsx
2833
+ import { getResponsiveFontSize } from '@retray-dev/ui-kit'
2834
+
2835
+ // Default steps: ≤10 chars → max, ≤12 → max-4, ≤14 → max-6, >14 → max-8
2836
+ const fontSize = getResponsiveFontSize(formatCOP(amount), 48)
2837
+
2838
+ <Text style={{ fontSize }} numberOfLines={1} adjustsFontSizeToFit>
2839
+ {formatCOP(amount)}
2840
+ </Text>
2841
+ ```
2842
+
2843
+ **Signature:**
2844
+ ```ts
2845
+ getResponsiveFontSize(
2846
+ text: string,
2847
+ maxSize: number,
2848
+ steps?: { maxLen: number; subtract: number }[]
2849
+ ): number
2850
+ ```
2851
+
2852
+ Custom steps example:
2853
+ ```tsx
2854
+ getResponsiveFontSize(text, 48, [
2855
+ { maxLen: 8, subtract: 0 },
2856
+ { maxLen: 11, subtract: 6 },
2857
+ { maxLen: 14, subtract: 10 },
2858
+ ])
2859
+ ```
2860
+
2861
+ **Alternative:** Use `<CurrencyDisplay autoScale maxFontSize={48} />` for currency values — no manual sizing needed.
2862
+
2863
+ ---
2864
+
2531
2865
  ## Hover Support (Web)
2532
2866
 
2533
2867
  ```tsx
@@ -2592,3 +2926,29 @@ All `Text` and `TextInput` components have `allowFontScaling={true}` — respect
2592
2926
 
2593
2927
  ### Scaling utilities (internal)
2594
2928
  Used internally — not exported. `s()` horizontal scale, `vs()` vertical scale, `ms()` moderate scale relative to 350×680 base.
2929
+
2930
+ ---
2931
+
2932
+ ## Full Composition Examples
2933
+
2934
+ The package includes **EXAMPLES.md** with complete, working code for 3 real-world app screens:
2935
+
2936
+ 1. **Finance Dashboard** — MonthPicker, CurrencyDisplay, Progress, CategoryStrip, ListItem, DetailRow
2937
+ 2. **Edit Profile** — Avatar, Badge, Input, Select, Switch, Card, AlertBanner
2938
+ 3. **Onboarding Flow** — Badge, Progress, Input, RadioGroup, EmptyState (multi-step wizard)
2939
+
2940
+ **For AI agents:** Import EXAMPLES.md from the package for full source code and patterns:
2941
+
2942
+ ```markdown
2943
+ ## UI Kit Composition Examples
2944
+ @./node_modules/@retray-dev/ui-kit/EXAMPLES.md
2945
+ ```
2946
+
2947
+ Each example includes:
2948
+ - Complete component code (copy-paste ready)
2949
+ - State management setup
2950
+ - Proper imports and theme usage
2951
+ - Common patterns (spacing, colors, navigation, toast feedback)
2952
+ - StyleSheet definitions
2953
+
2954
+ **For human developers:** Examples are also live in the `example/` app. Clone the repo, run `pnpm install && pnpm build`, then `cd example && pnpm start` to see them in action. Access via "🖥 App Screen Compositions" accordion.