@cyberkaidev/ui 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 (152) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +721 -0
  3. package/lib/module/ThemeProvider.js +84 -0
  4. package/lib/module/ThemeProvider.js.map +1 -0
  5. package/lib/module/components/AppBar.js +185 -0
  6. package/lib/module/components/AppBar.js.map +1 -0
  7. package/lib/module/components/BounceEffect.js +50 -0
  8. package/lib/module/components/BounceEffect.js.map +1 -0
  9. package/lib/module/components/Button.js +48 -0
  10. package/lib/module/components/Button.js.map +1 -0
  11. package/lib/module/components/Chips.js +94 -0
  12. package/lib/module/components/Chips.js.map +1 -0
  13. package/lib/module/components/IconButton.js +71 -0
  14. package/lib/module/components/IconButton.js.map +1 -0
  15. package/lib/module/components/InforList.js +92 -0
  16. package/lib/module/components/InforList.js.map +1 -0
  17. package/lib/module/components/LoadingApp.js +58 -0
  18. package/lib/module/components/LoadingApp.js.map +1 -0
  19. package/lib/module/components/Overlays.js +20 -0
  20. package/lib/module/components/Overlays.js.map +1 -0
  21. package/lib/module/components/RippleEffect.js +178 -0
  22. package/lib/module/components/RippleEffect.js.map +1 -0
  23. package/lib/module/components/Scaffold.js +172 -0
  24. package/lib/module/components/Scaffold.js.map +1 -0
  25. package/lib/module/components/SearchBar.js +161 -0
  26. package/lib/module/components/SearchBar.js.map +1 -0
  27. package/lib/module/components/SegmentedButton.js +151 -0
  28. package/lib/module/components/SegmentedButton.js.map +1 -0
  29. package/lib/module/components/ShapedButton.js +58 -0
  30. package/lib/module/components/ShapedButton.js.map +1 -0
  31. package/lib/module/components/SlideModal.js +196 -0
  32. package/lib/module/components/SlideModal.js.map +1 -0
  33. package/lib/module/components/TabBar.js +107 -0
  34. package/lib/module/components/TabBar.js.map +1 -0
  35. package/lib/module/components/Text.js +47 -0
  36. package/lib/module/components/Text.js.map +1 -0
  37. package/lib/module/components/TextButton.js +53 -0
  38. package/lib/module/components/TextButton.js.map +1 -0
  39. package/lib/module/components/TextInput.js +83 -0
  40. package/lib/module/components/TextInput.js.map +1 -0
  41. package/lib/module/components/Toast.js +101 -0
  42. package/lib/module/components/Toast.js.map +1 -0
  43. package/lib/module/hooks/useAppBarScroll.js +20 -0
  44. package/lib/module/hooks/useAppBarScroll.js.map +1 -0
  45. package/lib/module/hooks/usePressAndHoldRepeat.js +71 -0
  46. package/lib/module/hooks/usePressAndHoldRepeat.js.map +1 -0
  47. package/lib/module/icons/Icon.js +85 -0
  48. package/lib/module/icons/Icon.js.map +1 -0
  49. package/lib/module/index.js +42 -0
  50. package/lib/module/index.js.map +1 -0
  51. package/lib/module/package.json +1 -0
  52. package/lib/module/responsive.js +28 -0
  53. package/lib/module/responsive.js.map +1 -0
  54. package/lib/module/stores/useLoadingApp.js +14 -0
  55. package/lib/module/stores/useLoadingApp.js.map +1 -0
  56. package/lib/module/stores/useToast.js +24 -0
  57. package/lib/module/stores/useToast.js.map +1 -0
  58. package/lib/module/theme.js +86 -0
  59. package/lib/module/theme.js.map +1 -0
  60. package/lib/module/types.js +2 -0
  61. package/lib/module/types.js.map +1 -0
  62. package/lib/typescript/jest.setup.d.ts +2 -0
  63. package/lib/typescript/jest.setup.d.ts.map +1 -0
  64. package/lib/typescript/package.json +1 -0
  65. package/lib/typescript/src/ThemeProvider.d.ts +39 -0
  66. package/lib/typescript/src/ThemeProvider.d.ts.map +1 -0
  67. package/lib/typescript/src/components/AppBar.d.ts +33 -0
  68. package/lib/typescript/src/components/AppBar.d.ts.map +1 -0
  69. package/lib/typescript/src/components/BounceEffect.d.ts +12 -0
  70. package/lib/typescript/src/components/BounceEffect.d.ts.map +1 -0
  71. package/lib/typescript/src/components/Button.d.ts +24 -0
  72. package/lib/typescript/src/components/Button.d.ts.map +1 -0
  73. package/lib/typescript/src/components/Chips.d.ts +23 -0
  74. package/lib/typescript/src/components/Chips.d.ts.map +1 -0
  75. package/lib/typescript/src/components/IconButton.d.ts +14 -0
  76. package/lib/typescript/src/components/IconButton.d.ts.map +1 -0
  77. package/lib/typescript/src/components/InforList.d.ts +28 -0
  78. package/lib/typescript/src/components/InforList.d.ts.map +1 -0
  79. package/lib/typescript/src/components/LoadingApp.d.ts +8 -0
  80. package/lib/typescript/src/components/LoadingApp.d.ts.map +1 -0
  81. package/lib/typescript/src/components/Overlays.d.ts +12 -0
  82. package/lib/typescript/src/components/Overlays.d.ts.map +1 -0
  83. package/lib/typescript/src/components/RippleEffect.d.ts +18 -0
  84. package/lib/typescript/src/components/RippleEffect.d.ts.map +1 -0
  85. package/lib/typescript/src/components/Scaffold.d.ts +62 -0
  86. package/lib/typescript/src/components/Scaffold.d.ts.map +1 -0
  87. package/lib/typescript/src/components/SearchBar.d.ts +20 -0
  88. package/lib/typescript/src/components/SearchBar.d.ts.map +1 -0
  89. package/lib/typescript/src/components/SegmentedButton.d.ts +37 -0
  90. package/lib/typescript/src/components/SegmentedButton.d.ts.map +1 -0
  91. package/lib/typescript/src/components/ShapedButton.d.ts +18 -0
  92. package/lib/typescript/src/components/ShapedButton.d.ts.map +1 -0
  93. package/lib/typescript/src/components/SlideModal.d.ts +15 -0
  94. package/lib/typescript/src/components/SlideModal.d.ts.map +1 -0
  95. package/lib/typescript/src/components/TabBar.d.ts +20 -0
  96. package/lib/typescript/src/components/TabBar.d.ts.map +1 -0
  97. package/lib/typescript/src/components/Text.d.ts +26 -0
  98. package/lib/typescript/src/components/Text.d.ts.map +1 -0
  99. package/lib/typescript/src/components/TextButton.d.ts +15 -0
  100. package/lib/typescript/src/components/TextButton.d.ts.map +1 -0
  101. package/lib/typescript/src/components/TextInput.d.ts +22 -0
  102. package/lib/typescript/src/components/TextInput.d.ts.map +1 -0
  103. package/lib/typescript/src/components/Toast.d.ts +4 -0
  104. package/lib/typescript/src/components/Toast.d.ts.map +1 -0
  105. package/lib/typescript/src/hooks/useAppBarScroll.d.ts +10 -0
  106. package/lib/typescript/src/hooks/useAppBarScroll.d.ts.map +1 -0
  107. package/lib/typescript/src/hooks/usePressAndHoldRepeat.d.ts +21 -0
  108. package/lib/typescript/src/hooks/usePressAndHoldRepeat.d.ts.map +1 -0
  109. package/lib/typescript/src/icons/Icon.d.ts +64 -0
  110. package/lib/typescript/src/icons/Icon.d.ts.map +1 -0
  111. package/lib/typescript/src/index.d.ts +30 -0
  112. package/lib/typescript/src/index.d.ts.map +1 -0
  113. package/lib/typescript/src/responsive.d.ts +15 -0
  114. package/lib/typescript/src/responsive.d.ts.map +1 -0
  115. package/lib/typescript/src/stores/useLoadingApp.d.ts +14 -0
  116. package/lib/typescript/src/stores/useLoadingApp.d.ts.map +1 -0
  117. package/lib/typescript/src/stores/useToast.d.ts +25 -0
  118. package/lib/typescript/src/stores/useToast.d.ts.map +1 -0
  119. package/lib/typescript/src/theme.d.ts +51 -0
  120. package/lib/typescript/src/theme.d.ts.map +1 -0
  121. package/lib/typescript/src/types.d.ts +26 -0
  122. package/lib/typescript/src/types.d.ts.map +1 -0
  123. package/package.json +189 -0
  124. package/src/ThemeProvider.tsx +108 -0
  125. package/src/components/AppBar.tsx +226 -0
  126. package/src/components/BounceEffect.tsx +53 -0
  127. package/src/components/Button.tsx +70 -0
  128. package/src/components/Chips.tsx +115 -0
  129. package/src/components/IconButton.tsx +84 -0
  130. package/src/components/InforList.tsx +110 -0
  131. package/src/components/LoadingApp.tsx +55 -0
  132. package/src/components/Overlays.tsx +21 -0
  133. package/src/components/RippleEffect.tsx +244 -0
  134. package/src/components/Scaffold.tsx +219 -0
  135. package/src/components/SearchBar.tsx +166 -0
  136. package/src/components/SegmentedButton.tsx +206 -0
  137. package/src/components/ShapedButton.tsx +74 -0
  138. package/src/components/SlideModal.tsx +232 -0
  139. package/src/components/TabBar.tsx +122 -0
  140. package/src/components/Text.tsx +70 -0
  141. package/src/components/TextButton.tsx +57 -0
  142. package/src/components/TextInput.tsx +108 -0
  143. package/src/components/Toast.tsx +95 -0
  144. package/src/hooks/useAppBarScroll.ts +19 -0
  145. package/src/hooks/usePressAndHoldRepeat.ts +85 -0
  146. package/src/icons/Icon.tsx +51 -0
  147. package/src/index.tsx +64 -0
  148. package/src/responsive.ts +32 -0
  149. package/src/stores/useLoadingApp.ts +19 -0
  150. package/src/stores/useToast.ts +36 -0
  151. package/src/theme.ts +125 -0
  152. package/src/types.ts +27 -0
package/README.md ADDED
@@ -0,0 +1,721 @@
1
+ # @cyberkaidev/ui
2
+
3
+ A themeable React Native component library. Expo-first, TypeScript-first.
4
+
5
+ ## Contents
6
+
7
+ - [Installation](#installation)
8
+ - [Setup](#setup)
9
+ - [Theming](#theming)
10
+ - [Components](#components)
11
+ - [Text & buttons](#text--buttons)
12
+ - [Inputs](#inputs)
13
+ - [Layout — `Scaffold`](#layout--scaffold)
14
+ - [Fixed bars — `AppBar`, `TabBar`, `SearchBar`](#fixed-bars--appbar-tabbar-searchbar)
15
+ - [Lists — `InforList`](#lists--inforlist)
16
+ - [Overlays — `Overlays`, `SlideModal`, `Toast`, `LoadingApp`](#overlays--overlays-slidemodal-toast-loadingapp)
17
+ - [Touch effects — `BounceEffect`, `RippleEffect`](#touch-effects--bounceeffect-rippleeffect)
18
+ - [Icons](#icons)
19
+ - [Hooks](#hooks)
20
+ - [Global state](#global-state)
21
+ - [Responsive helpers](#responsive-helpers)
22
+ - [Exported types](#exported-types)
23
+ - [TypeScript & config notes](#typescript--config-notes)
24
+
25
+ ---
26
+
27
+ ## Installation
28
+
29
+ ```sh
30
+ yarn add @cyberkaidev/ui
31
+ ```
32
+
33
+ ### Peer dependencies
34
+
35
+ Install these in the consuming app:
36
+
37
+ ```sh
38
+ yarn expo install react-native-reanimated react-native-worklets react-native-safe-area-context \
39
+ @shopify/flash-list expo-symbols expo-blur expo-constants
40
+ ```
41
+
42
+ - `react-native-reanimated` needs its Babel plugin — `babel-preset-expo` adds it automatically on
43
+ Expo SDK 54+. On bare React Native, add `react-native-worklets/plugin` (last) to `babel.config.js`.
44
+ - `zustand` ships as a normal dependency — you don't install it.
45
+
46
+ ---
47
+
48
+ ## Setup
49
+
50
+ Wrap the app **once** in `SafeAreaProvider` and `ThemeProvider`, and mount `Overlays` at the
51
+ root.
52
+
53
+ ```tsx
54
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
55
+ import { ThemeProvider, Overlays, colors } from '@cyberkaidev/ui';
56
+
57
+ export default function App() {
58
+ return (
59
+ <SafeAreaProvider>
60
+ <ThemeProvider colors={colors}>
61
+ <RootNavigator />
62
+ <Overlays loadingLabel="Loading…" />
63
+ </ThemeProvider>
64
+ </SafeAreaProvider>
65
+ );
66
+ }
67
+ ```
68
+
69
+ - **`ThemeProvider` is required, and its `colors` prop is required** — there is no default
70
+ palette. Pass the exported `colors` (a ready-made palette), spread from it, or provide your own
71
+ `ColorsType`. Any component rendered outside a `ThemeProvider` throws.
72
+ - **`SafeAreaProvider` is required.** `Scaffold.ScrollView` / `Scaffold.FlashList`, `TabBar` and
73
+ `SlideModal` read safe-area insets and throw without it.
74
+ - **`Overlays`** renders the app-wide `Toast` + `LoadingApp` (see [`Overlays`](#overlays)).
75
+ Mount it once at the root, inside `ThemeProvider`; trigger them from anywhere with `useToast` /
76
+ `useLoadingApp`. You can also mount `<Toast />` / `<LoadingApp />` individually if you need to.
77
+ - **Fonts.** Components reference the `Inter-Regular` / `Inter-Medium` / `Inter-Bold` families.
78
+ Load them in the app (e.g. `expo-font`); without them, text falls back to the system font.
79
+
80
+ ---
81
+
82
+ ## Theming
83
+
84
+ The theme has six token groups: `colors` (**required**, full palette) plus `fontSizes`,
85
+ `iconSize`, `spaces`, `borderRadius`, `pageSpacing` (**optional**, partial — a missing key is
86
+ inherited from the nearest parent provider, or from the built-in defaults). Providers nest.
87
+
88
+ ```tsx
89
+ import { colors } from '@cyberkaidev/ui';
90
+
91
+ <ThemeProvider
92
+ colors={{ ...colors, primary: '#ff8a00', background: '#0b0b0f' }} // full ColorsType
93
+ fontSizes={{ m: 20 }} // partial
94
+ spaces={{ vertical: { m: 24 } }} // deep-merged one level
95
+ borderRadius={{ radius_20: 8 }}
96
+ >
97
+ <Screen />
98
+ </ThemeProvider>
99
+ ```
100
+
101
+ A **nested** `ThemeProvider` also needs a full `colors` (it replaces the palette); the other
102
+ groups still merge over the parent.
103
+
104
+ ### Reading the theme
105
+
106
+ | API | Returns | Use for |
107
+ |---|---|---|
108
+ | `useColors()` | `ColorsType` | color values inside a component (shortcut for `useTheme().colors`) |
109
+ | `useTheme()` | `ThemeType` | any token group inside a component |
110
+ | `useThemedStyles(makeStyles)` | `T` | a `StyleSheet` built from the resolved theme, memoized against it |
111
+
112
+ ```tsx
113
+ import { StyleSheet } from 'react-native';
114
+ import { useTheme, useThemedStyles, type ThemeType } from '@cyberkaidev/ui';
115
+
116
+ function Card() {
117
+ const { colors } = useTheme();
118
+ const styles = useThemedStyles(makeStyles);
119
+ return <View style={[styles.card, { borderColor: colors.border }]} />;
120
+ }
121
+
122
+ // module-level factory — keep it out of the component so the memo stays stable
123
+ function makeStyles(t: ThemeType) {
124
+ return StyleSheet.create({
125
+ card: {
126
+ padding: t.spaces.horizontal.m,
127
+ borderRadius: t.borderRadius.radius_15,
128
+ backgroundColor: t.colors.surface,
129
+ },
130
+ });
131
+ }
132
+ ```
133
+
134
+ ### Exported token objects
135
+
136
+ - **`colors`** — a ready-made `ColorsType` palette. Not a default: you must still pass a palette
137
+ to `ThemeProvider`, but this is a fine one to pass.
138
+ - **`fontSizes`, `iconSize`, `spaces`, `borderRadius`, `pageSpacing`** — the built-in defaults for
139
+ those groups (`ThemeProvider` merges your partials on top). `defaultTokens` bundles all five.
140
+
141
+ ```ts
142
+ export const colors = {
143
+ background: 'black',
144
+ background_60: 'rgba(0,0,0,0.6)',
145
+ on_surface: '#1A191E',
146
+ surface: '#100e12',
147
+ surface_90: 'rgba(16, 14, 18, 0.9)',
148
+ border: '#3D3B40',
149
+ text: '#f6f1ff',
150
+ primary: '#4284ff',
151
+ red: '#ff0004',
152
+ green: '#00ff2a',
153
+ amber: '#FFC107',
154
+ transparent: 'transparent',
155
+ };
156
+
157
+ // fontSizes / iconSize: keys are xxxl…xs (fontSizes) and xl…s (iconSize), values in dp
158
+ // spaces: { horizontal: { xs…xxl }, vertical: { xs…xxl } } in dp
159
+ // borderRadius: { radius_5, radius_10, radius_15, radius_20, radius_100 }
160
+ // pageSpacing: { horizontal, top, bottom }
161
+ ```
162
+
163
+ > A **dark/light** theme is the app's job: keep two `ColorsType` palettes and swap which one you
164
+ > pass to `ThemeProvider`.
165
+
166
+ ---
167
+
168
+ ## Components
169
+
170
+ Every component takes an optional `testID`. Every value shown as a token (`FontSizeType`,
171
+ `ColorsType` keys, …) resolves against the active theme.
172
+
173
+ ### Text & buttons
174
+
175
+ #### `Text`
176
+
177
+ Themed text primitive. All copy comes from `children`.
178
+
179
+ | Prop | Type | Default | Notes |
180
+ |---|---|---|---|
181
+ | `children` | `ReactNode` | — | |
182
+ | `weight` | `'regular' \| 'medium' \| 'bold'` | `'regular'` | → `Inter-*` family |
183
+ | `size` | `FontSizeType` (`xs`…`xxxl`) | `'s'` | |
184
+ | `color` | `string` | theme `text` | |
185
+ | `numberOfLines` | `number` | — | truncation |
186
+ | `textAlign` | `'left' \| 'center' \| 'right'` | — | |
187
+ | `marginT` / `marginB` / `marginL` / `marginR` | `number` | `0` | dp |
188
+ | `opacity` | `number` | — | |
189
+
190
+ ```tsx
191
+ <Text weight="bold" size="l">Title</Text>
192
+ <Text color="#8a8a8a" numberOfLines={2}>Subtitle that may wrap…</Text>
193
+ ```
194
+
195
+ #### `Button`
196
+
197
+ Base pressable. iOS gets opacity feedback; Android gets a touch ripple. Style it yourself — the
198
+ other buttons wrap this.
199
+
200
+ | Prop | Type | Notes |
201
+ |---|---|---|
202
+ | `children` | `ReactNode` | |
203
+ | `onPress` / `onPressIn` / `onPressOut` | `() => void` | |
204
+ | `disabled` | `boolean` | |
205
+ | `style` | `StyleProp<ViewStyle>` | |
206
+ | `hitSlop` | `Insets` | |
207
+ | `rippleColor` / `rippleContainerBorderRadius` | `string` / `number` | Android only |
208
+
209
+ #### `ShapedButton`
210
+
211
+ Filled call-to-action with a loading state.
212
+
213
+ | Prop | Type | Default |
214
+ |---|---|---|
215
+ | `children` | `ReactNode` | — |
216
+ | `onPress` | `() => void` | — (required) |
217
+ | `type` | `'primary' \| 'secondary'` | `'primary'` |
218
+ | `size` | `FontSizeType` | — |
219
+ | `disabled` | `boolean` | — |
220
+ | `isLoading` | `boolean` | `false` |
221
+
222
+ ```tsx
223
+ <ShapedButton isLoading={saving} onPress={save}>Save</ShapedButton>
224
+ <ShapedButton type="secondary" onPress={cancel}>Cancel</ShapedButton>
225
+ ```
226
+
227
+ #### `TextButton`
228
+
229
+ Text-only button, no fill.
230
+
231
+ | Prop | Type | Default |
232
+ |---|---|---|
233
+ | `children` | `ReactNode` | — |
234
+ | `onPress` | `() => void` | — (required) |
235
+ | `color` | `string` | theme `primary` |
236
+ | `size` | `FontSizeType` | — |
237
+ | `disabled` | `boolean` | `false` |
238
+
239
+ #### `IconButton`
240
+
241
+ Circular icon-only button with an optional "active" dot.
242
+
243
+ | Prop | Type | Default |
244
+ |---|---|---|
245
+ | `icon` | `IconNameType` | — (required) |
246
+ | `onPress` | `() => void` | — (required) |
247
+ | `active` | `boolean` | `false` |
248
+ | `disabled` | `boolean` | `false` |
249
+
250
+ ```tsx
251
+ <IconButton icon="filter" active={hasFilters} onPress={openFilters} />
252
+ ```
253
+
254
+ ### Inputs
255
+
256
+ #### `Chips`
257
+
258
+ Horizontally scrolling row of selectable chips. Generic over the item key type.
259
+
260
+ | Prop | Type | Default |
261
+ |---|---|---|
262
+ | `data` | `{ key: Item; label: string; disabled?: boolean; prefix?: ReactNode; scheme?: 'primary' \| 'surface' \| 'on_surface' }[]` | — (`scheme` defaults to `'surface'`) |
263
+ | `onPress` | `(key: Item) => void` | — (omit → non-interactive) |
264
+ | `marginT` / `marginB` | `number` | `0` |
265
+
266
+ ```tsx
267
+ <Chips
268
+ data={[
269
+ { key: 'all', label: 'All' },
270
+ { key: 'new', label: 'New', scheme: 'primary' },
271
+ { key: 'used', label: 'Used', disabled: true },
272
+ ]}
273
+ onPress={setFilter}
274
+ />
275
+ ```
276
+
277
+ #### `TextInput`
278
+
279
+ Single-line field with a supporting message and error state.
280
+
281
+ | Prop | Type | Default |
282
+ |---|---|---|
283
+ | `value` | `string` | — (required) |
284
+ | `placeholder` | `string` | — (required) |
285
+ | `onChangeText` | `(text: string) => void` | — (required) |
286
+ | `supportingText` | `string` | — (shown in the error color) |
287
+ | `isError` | `boolean` | `false` |
288
+ | `onEnter` | `() => void` | fired on submit when the value is non-empty |
289
+ | `onBlur` | `TextInputProps['onBlur']` | — |
290
+ | `autoFocus` / `autoCapitalize` / `autoCorrect` / `autoComplete` / `inputMode` | passthrough to RN `TextInput` | — |
291
+
292
+ ```tsx
293
+ <TextInput
294
+ value={name}
295
+ placeholder="Your name"
296
+ supportingText={name.length < 2 ? 'At least 2 characters' : undefined}
297
+ isError={touched && name.length < 2}
298
+ onChangeText={setName}
299
+ onEnter={submit}
300
+ />
301
+ ```
302
+
303
+ #### `SegmentedButton`
304
+
305
+ Single-select segmented button in the Material 3 **Expressive** style — a rounded track holding
306
+ equal-width segments. The selected segment morphs into a filled pill with a springy pop, and every
307
+ segment squishes on press. Controlled, generic over the segment `value` type. Best with 2–5 segments.
308
+
309
+ | Prop | Type | Default |
310
+ |---|---|---|
311
+ | `segments` | `{ value: Value; label: string; icon?: IconNameType; disabled?: boolean }[]` | — (required) |
312
+ | `value` | `Value` | — (required) the selected segment |
313
+ | `onChange` | `(value: Value) => void` | — (required) |
314
+ | `size` | `'s' \| 'm' \| 'l'` | `'m'` (track height scale) |
315
+ | `disabled` | `boolean` | `false` (disables the whole control) |
316
+ | `showSelectedCheck` | `boolean` | `true` (leading check on the selected segment) |
317
+
318
+ Export: `SEGMENTED_BUTTON_HEIGHT` (the `'m'` track height).
319
+
320
+ > The classic outlined-with-dividers Material 3 version is archived at
321
+ > [`docs/checkpoints/segmented-button-m3.md`](docs/checkpoints/segmented-button-m3.md).
322
+
323
+ ```tsx
324
+ const [range, setRange] = useState<'day' | 'week' | 'month'>('week');
325
+
326
+ <SegmentedButton
327
+ value={range}
328
+ onChange={setRange}
329
+ segments={[
330
+ { value: 'day', label: 'Day' },
331
+ { value: 'week', label: 'Week' },
332
+ { value: 'month', label: 'Month' },
333
+ ]}
334
+ />;
335
+ ```
336
+
337
+ ### Layout — `Scaffold`
338
+
339
+ Compound component for page structure.
340
+
341
+ | Piece | Props | Purpose |
342
+ |---|---|---|
343
+ | `Scaffold.Body` | `children`, `surface?: boolean` | full-screen container; `surface` uses the elevated background |
344
+ | `Scaffold.View` | `children`, `paddingHorizontal?`, `paddingTop?` | non-scrolling region with page insets |
345
+ | `Scaffold.ScrollView` | `spaceTopBar`, `spaceBottomBar`, `children`, `paddingHorizontal?`, `paddingTop?`, `paddingBottom?`, `onScroll?` | scrolling region (reanimated) |
346
+ | `Scaffold.FlashList<T>` | `spaceTopBar`, `spaceBottomBar`, all `@shopify/flash-list` props + `paddingHorizontal?`, `paddingTop?`, `paddingBottom?`, `onScroll?`, `drawDistance?` | virtualized list |
347
+
348
+ `spaceTopBar` (`'large' | 'small' | 'none'`) and `spaceBottomBar` (`boolean`) are **required** on
349
+ `Scaffold.ScrollView` and `Scaffold.FlashList` — they reserve the top/bottom insets for the
350
+ screen's fixed bars so you never pass `APP_BAR_HEIGHT` / `useTabBarHeight()` by hand:
351
+
352
+ | `spaceTopBar` | top inset |
353
+ |---|---|
354
+ | `'large'` | `APP_BAR_LARGE_HEIGHT` — for `<AppBar large />` |
355
+ | `'small'` | `APP_BAR_HEIGHT` — for a fixed `<AppBar />` / `<SearchBar />` |
356
+ | `'none'` | `pageSpacing.top` |
357
+
358
+ `spaceBottomBar={true}` adds the `TabBar` height (incl. bottom safe-area) to the bottom inset;
359
+ `false` keeps just `pageSpacing.bottom` + safe-area. The numeric `paddingTop` / `paddingBottom` props
360
+ still win when passed. `onScroll` takes a reanimated scroll handler (see [`useAppBarScroll`](#hooks)).
361
+
362
+ `Scaffold.View` is non-scrolling — its content never slides under the fixed bars — so it takes
363
+ neither prop; pass an explicit `paddingTop` if a bar covers it.
364
+
365
+ ```tsx
366
+ <Scaffold.Body>
367
+ <AppBar title="Collection" large scrollY={scrollY} />
368
+ <Scaffold.FlashList
369
+ spaceTopBar="large"
370
+ spaceBottomBar
371
+ data={cards}
372
+ keyExtractor={(c) => c.id}
373
+ renderItem={({ item }) => <Row card={item} />}
374
+ onScroll={scrollHandler}
375
+ />
376
+ </Scaffold.Body>
377
+ ```
378
+
379
+ ### Fixed bars — `AppBar`, `TabBar`, `SearchBar`
380
+
381
+ All three are `position: absolute` — render them as siblings of your scrollable content inside a
382
+ `Scaffold.Body`. The content region reserves the matching inset via `spaceTopBar` / `spaceBottomBar`
383
+ (see above); the height constants below are exported for manual layouts.
384
+
385
+ #### `AppBar`
386
+
387
+ | Prop | Type | Notes |
388
+ |---|---|---|
389
+ | `title` | `string` | — |
390
+ | `enabledBack` | `boolean` | shows the back control |
391
+ | `onBack` | `() => void` | called by the back control |
392
+ | `large` | `boolean` | large title that collapses on scroll |
393
+ | `trailing` | `AppBarTrailingType` | right-side action |
394
+ | `scrollY` | `SharedValue<number>` | drives the large-title collapse |
395
+
396
+ `AppBarTrailingType = { label?: string; icon?: IconNameType; tintColor?: ColorValue; hidden?: boolean; onPress: () => void }`
397
+
398
+ Exports: `APP_BAR_HEIGHT`, `APP_BAR_LARGE_HEIGHT` (computed from the **default** `pageSpacing`).
399
+
400
+ ```tsx
401
+ const { scrollY, scrollHandler } = useAppBarScroll();
402
+
403
+ <AppBar
404
+ title="Settings"
405
+ enabledBack
406
+ onBack={router.back}
407
+ trailing={{ icon: 'check', onPress: save }}
408
+ scrollY={scrollY}
409
+ />
410
+ ```
411
+
412
+ #### `TabBar`
413
+
414
+ Data-driven — it knows nothing about routing.
415
+
416
+ | Prop | Type | Default |
417
+ |---|---|---|
418
+ | `items` | `TabItemType[]` | — |
419
+ | `visible` | `boolean` | `true` (fades out + disables touches when `false`) |
420
+
421
+ `TabItemType = { key: string; icon: IconNameType; label: string; active: boolean; onPress: () => void }`
422
+
423
+ Export: `useTabBarHeight()` — the height a screen should reserve at the bottom.
424
+
425
+ ```tsx
426
+ <TabBar
427
+ items={[
428
+ { key: 'home', icon: 'folder', label: 'Home', active: route === 'home', onPress: () => go('home') },
429
+ { key: 'search', icon: 'search', label: 'Search', active: route === 'search', onPress: () => go('search') },
430
+ ]}
431
+ visible={!keyboardOpen}
432
+ />
433
+ ```
434
+
435
+ #### `SearchBar`
436
+
437
+ | Prop | Type | Notes |
438
+ |---|---|---|
439
+ | `placeholder` | `string` | — |
440
+ | `onEnter` | `(query: string) => void` | fired with the trimmed query on submit |
441
+ | `onBack` | `() => void` | back control |
442
+ | `editable` | `boolean` | — |
443
+ | `autoFocus` | `boolean` | — |
444
+
445
+ Export: `SEARCH_BAR_HEIGHT`.
446
+
447
+ ### Lists — `InforList`
448
+
449
+ #### `InforList`
450
+
451
+ Grouped list with dividers between rows. Compound.
452
+
453
+ | Piece | Props |
454
+ |---|---|
455
+ | `InforList.Container` | `children`, `marginT?`, `marginB?` |
456
+ | `InforList.Content` | `children`, `onPress?` (pressable when provided) |
457
+ | `InforList.Title` | `children`, `color?` |
458
+
459
+ ```tsx
460
+ <InforList.Container>
461
+ <InforList.Content onPress={openProfile}>
462
+ <InforList.Title>Name</InforList.Title>
463
+ <InforList.Title color={colors.primary}>Ada</InforList.Title>
464
+ </InforList.Content>
465
+ <InforList.Content>
466
+ <InforList.Title>Role</InforList.Title>
467
+ <InforList.Title>Engineer</InforList.Title>
468
+ </InforList.Content>
469
+ </InforList.Container>
470
+ ```
471
+
472
+ ### Overlays — `Overlays`, `SlideModal`, `Toast`, `LoadingApp`
473
+
474
+ #### `Overlays`
475
+
476
+ The app-wide overlays (`Toast` + `LoadingApp`) in one component. **Mount once at the root**,
477
+ inside `ThemeProvider`, as a sibling of your navigator — they render above everything and are
478
+ driven from anywhere via `useToast` / `useLoadingApp`.
479
+
480
+ | Prop | Type | Notes |
481
+ |---|---|---|
482
+ | `loadingLabel` | `string` | forwarded to `LoadingApp`'s `label` |
483
+
484
+ ```tsx
485
+ <ThemeProvider colors={colors}>
486
+ <RootNavigator />
487
+ <Overlays loadingLabel="Loading…" />
488
+ </ThemeProvider>
489
+ ```
490
+
491
+ Use `<Toast />` / `<LoadingApp />` directly only if you need them mounted separately.
492
+
493
+ #### `SlideModal`
494
+
495
+ Bottom sheet, controlled through a `ref`.
496
+
497
+ | Prop | Type | Default |
498
+ |---|---|---|
499
+ | `children` | `ReactNode` | — |
500
+ | `title` | `string` | — (required) |
501
+ | `isLoading` | `boolean` | `false` (swaps content for a spinner, blocks drag-to-dismiss) |
502
+
503
+ `ref` exposes `SlideModalRef = { show: () => void; hide: () => void }`.
504
+
505
+ ```tsx
506
+ const modal = useRef<SlideModalRef>(null);
507
+
508
+ <ShapedButton onPress={() => modal.current?.show()}>Open</ShapedButton>
509
+ <SlideModal ref={modal} title="Edit">
510
+ <EditForm onDone={() => modal.current?.hide()} />
511
+ </SlideModal>
512
+ ```
513
+
514
+ #### `Toast`
515
+
516
+ Transient centered notice, auto-hides after ~3s. Mounted via [`Overlays`](#overlays) (or on its
517
+ own once at the root); triggered from anywhere via [`useToast`](#global-state).
518
+
519
+ ```tsx
520
+ useToast().showToast({ message: 'Saved', variant: 'success' });
521
+ // variant: 'error' | 'success' | 'info' (default 'info')
522
+ ```
523
+
524
+ #### `LoadingApp`
525
+
526
+ Blocking full-screen loading overlay. Mounted via [`Overlays`](#overlays) (or on its own once at
527
+ the root); driven from anywhere via [`useLoadingApp`](#global-state).
528
+
529
+ | Prop | Type | Notes |
530
+ |---|---|---|
531
+ | `label` | `string` | optional caption under the spinner |
532
+
533
+ ```tsx
534
+ useLoadingApp().setLoadingVisible(true);
535
+ ```
536
+
537
+ ### Touch effects — `BounceEffect`, `RippleEffect`
538
+
539
+ #### `BounceEffect`
540
+
541
+ Wraps a child and plays a scale "bounce" on press, firing the callback on release.
542
+
543
+ | Prop | Type |
544
+ |---|---|
545
+ | `onActionTrigger` | `() => void` |
546
+ | `children` | `ReactNode` |
547
+ | `style` | `PressableProps['style']` |
548
+
549
+ #### `RippleEffect`
550
+
551
+ Wraps a child and paints a touch ripple at the press point. Accepts all `Pressable` props plus:
552
+
553
+ | Prop | Type | Default |
554
+ |---|---|---|
555
+ | `rippleColor` | `string` | theme `text` |
556
+ | `rippleOpacity` | `number` | `0.3` |
557
+ | `rippleDuration` | `number` | `400` |
558
+ | `rippleSize` | `number` | `0` (auto) |
559
+ | `rippleContainerBorderRadius` | `number` | `0` |
560
+ | `rippleCentered` | `boolean` | `false` |
561
+ | `rippleSequential` | `boolean` | `false` |
562
+ | `rippleFades` | `boolean` | `true` |
563
+
564
+ ---
565
+
566
+ ## Icons
567
+
568
+ `Icon` renders a curated set of UI glyphs via `expo-symbols` (`SymbolView`).
569
+
570
+ | Prop | Type | Default |
571
+ |---|---|---|
572
+ | `name` | `IconNameType` | — (required) |
573
+ | `color` | `string` | theme `primary` |
574
+ | `size` | `number` (dp) | `iconSize.m` |
575
+
576
+ ```tsx
577
+ import { Icon, iconSize } from '@cyberkaidev/ui';
578
+
579
+ <Icon name="search" />
580
+ <Icon name="check" color="#00ff2a" size={iconSize.l} />
581
+ ```
582
+
583
+ **Available names:** `arrow_back`, `search`, `close`, `add`, `remove`, `edit`, `check`, `error`,
584
+ `filter`, `settings`, `folder`, `question`.
585
+
586
+ ---
587
+
588
+ ## Hooks
589
+
590
+ ### `useAppBarScroll()`
591
+
592
+ Wires a collapsing `AppBar` to a scroll region.
593
+
594
+ ```tsx
595
+ const { scrollY, scrollHandler } = useAppBarScroll();
596
+
597
+ <Scaffold.Body>
598
+ <AppBar title="Feed" large scrollY={scrollY} />
599
+ <Scaffold.ScrollView spaceTopBar="large" spaceBottomBar onScroll={scrollHandler}>
600
+ {/* … */}
601
+ </Scaffold.ScrollView>
602
+ </Scaffold.Body>;
603
+ ```
604
+
605
+ Returns `{ scrollY: SharedValue<number>, scrollHandler }`.
606
+
607
+ ### `usePressAndHoldRepeat({ onTrigger, disabled? })`
608
+
609
+ Press-and-hold with acceleration. Fires `onTrigger` once on press, then repeatedly (speeding up)
610
+ while held. Clamping is the caller's job.
611
+
612
+ ```tsx
613
+ const { onPressIn, onPressOut } = usePressAndHoldRepeat({
614
+ onTrigger: () => setCount((c) => Math.min(c + 1, max)),
615
+ disabled: count >= max,
616
+ });
617
+
618
+ <Button onPressIn={onPressIn} onPressOut={onPressOut}>
619
+ <Icon name="add" />
620
+ </Button>;
621
+ ```
622
+
623
+ ---
624
+
625
+ ## Global state
626
+
627
+ Two [zustand](https://zustand.docs.pmnd.rs/) stores. Use the hook inside components, or
628
+ `.getState()` anywhere.
629
+
630
+ ### `useToast`
631
+
632
+ ```ts
633
+ useToast(): {
634
+ visible: boolean;
635
+ variant: 'error' | 'success' | 'info';
636
+ message: string;
637
+ showToast: (args: { message: string; variant?: 'error' | 'success' | 'info' }) => void;
638
+ hideToast: () => void;
639
+ };
640
+ ```
641
+
642
+ ### `useLoadingApp`
643
+
644
+ ```ts
645
+ useLoadingApp(): {
646
+ visible: boolean;
647
+ setLoadingVisible: (visible: boolean) => void;
648
+ };
649
+ ```
650
+
651
+ ```ts
652
+ // outside React (e.g. an API layer)
653
+ import { useLoadingApp } from '@cyberkaidev/ui';
654
+
655
+ async function withLoading<T>(fn: () => Promise<T>) {
656
+ useLoadingApp.getState().setLoadingVisible(true);
657
+ try {
658
+ return await fn();
659
+ } finally {
660
+ useLoadingApp.getState().setLoadingVisible(false);
661
+ }
662
+ }
663
+ ```
664
+
665
+ ---
666
+
667
+ ## Responsive helpers
668
+
669
+ ```ts
670
+ import { widthDP, heightDP } from '@cyberkaidev/ui';
671
+
672
+ widthDP(50); // 50% of screen width, in dp, rounded to the nearest pixel
673
+ heightDP(4.3); // 4.3% of screen height
674
+ ```
675
+
676
+ The theme's spacing / font-size / icon-size scales are built from these. Screen size is read once
677
+ at module load — values don't react to rotation.
678
+
679
+ ---
680
+
681
+ ## Exported types
682
+
683
+ | Type | Shape |
684
+ |---|---|
685
+ | `FontSizeType` | `'xs' \| 's' \| 'm' \| 'l' \| 'xl' \| 'xxl' \| 'xxxl'` |
686
+ | `IconSizeType` | `'s' \| 'm' \| 'l' \| 'xl'` |
687
+ | `FontWeightType` | `'regular' \| 'medium' \| 'bold'` |
688
+ | `TextAlignType` | `'left' \| 'center' \| 'right'` |
689
+ | `ToastVariantType` | `'error' \| 'success' \| 'info'` |
690
+ | `ColorsType` | the 12-key palette shape |
691
+ | `ThemeType` | `{ colors, fontSizes, iconSize, spaces, borderRadius, pageSpacing }` |
692
+ | `FontSizesType` / `IconSizesType` / `SpacesType` / `SpaceScaleType` / `BorderRadiusType` / `PageSpacingType` | individual token-group shapes |
693
+ | `ThemeTokensType` | `ThemeType` without `colors` (the shape of `defaultTokens`) |
694
+ | `IconNameType` | union of the icon names above |
695
+ | `SlideModalRef` | `{ show(): void; hide(): void }` |
696
+ | `TabItemType` | tab-bar item shape |
697
+ | `AppBarTrailingType` | app-bar trailing action shape |
698
+ | `SpaceTopBarType` | `'large' \| 'small' \| 'none'` — `Scaffold` `spaceTopBar` |
699
+ | `SegmentedButtonItemType<Value>` | segmented-button segment shape |
700
+ | `SegmentedButtonSizeType` | `'s' \| 'm' \| 'l'` — `SegmentedButton` `size` |
701
+
702
+ ---
703
+
704
+ ## TypeScript & config notes
705
+
706
+ - Ships ESM + `.d.ts` (built with `react-native-builder-bob`); single entry point
707
+ `@cyberkaidev/ui`.
708
+ - No `moduleSuffixes` needed in the consumer's `tsconfig` — `Button` is a single file with a
709
+ `Platform.OS` branch.
710
+ - `Text` referencing `Inter-*` is a soft dependency: unregistered fonts fall back to the system
711
+ font, no crash.
712
+
713
+ ## Contributing
714
+
715
+ - [Development workflow](CONTRIBUTING.md#development-workflow)
716
+ - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
717
+ - [Code of conduct](CODE_OF_CONDUCT.md)
718
+
719
+ ## License
720
+
721
+ MIT