@nexub/react-native 0.0.0-stage → 2.2.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 (134) hide show
  1. package/README.md +113 -2
  2. package/lib/module/components/actions.js +222 -0
  3. package/lib/module/components/actions.js.map +1 -0
  4. package/lib/module/components/containers.js +300 -0
  5. package/lib/module/components/containers.js.map +1 -0
  6. package/lib/module/components/display.js +299 -0
  7. package/lib/module/components/display.js.map +1 -0
  8. package/lib/module/components/feedback.js +484 -0
  9. package/lib/module/components/feedback.js.map +1 -0
  10. package/lib/module/components/inputs.js +792 -0
  11. package/lib/module/components/inputs.js.map +1 -0
  12. package/lib/module/components/mobile.js +818 -0
  13. package/lib/module/components/mobile.js.map +1 -0
  14. package/lib/module/components/overlays.js +421 -0
  15. package/lib/module/components/overlays.js.map +1 -0
  16. package/lib/module/components/shared.js +57 -0
  17. package/lib/module/components/shared.js.map +1 -0
  18. package/lib/module/hooks/useControllableState.js +23 -0
  19. package/lib/module/hooks/useControllableState.js.map +1 -0
  20. package/lib/module/index.js +31 -0
  21. package/lib/module/index.js.map +1 -0
  22. package/lib/module/package.json +1 -0
  23. package/lib/module/patterns/app.js +709 -0
  24. package/lib/module/patterns/app.js.map +1 -0
  25. package/lib/module/patterns/auth.js +224 -0
  26. package/lib/module/patterns/auth.js.map +1 -0
  27. package/lib/module/patterns/home.js +201 -0
  28. package/lib/module/patterns/home.js.map +1 -0
  29. package/lib/module/patterns/index.js +8 -0
  30. package/lib/module/patterns/index.js.map +1 -0
  31. package/lib/module/patterns/money.js +419 -0
  32. package/lib/module/patterns/money.js.map +1 -0
  33. package/lib/module/primitives/Icon.js +17 -0
  34. package/lib/module/primitives/Icon.js.map +1 -0
  35. package/lib/module/primitives/Layout.js +79 -0
  36. package/lib/module/primitives/Layout.js.map +1 -0
  37. package/lib/module/primitives/Pressable.js +79 -0
  38. package/lib/module/primitives/Pressable.js.map +1 -0
  39. package/lib/module/primitives/ScreenContainer.js +58 -0
  40. package/lib/module/primitives/ScreenContainer.js.map +1 -0
  41. package/lib/module/primitives/Surface.js +57 -0
  42. package/lib/module/primitives/Surface.js.map +1 -0
  43. package/lib/module/primitives/Text.js +70 -0
  44. package/lib/module/primitives/Text.js.map +1 -0
  45. package/lib/module/theme/ThemeProvider.js +127 -0
  46. package/lib/module/theme/ThemeProvider.js.map +1 -0
  47. package/lib/module/theme/fonts.js +56 -0
  48. package/lib/module/theme/fonts.js.map +1 -0
  49. package/lib/module/theme/haptics.js +56 -0
  50. package/lib/module/theme/haptics.js.map +1 -0
  51. package/lib/module/theme/motion.js +89 -0
  52. package/lib/module/theme/motion.js.map +1 -0
  53. package/lib/module/theme/navigation.js +99 -0
  54. package/lib/module/theme/navigation.js.map +1 -0
  55. package/lib/typescript/components/actions.d.ts +53 -0
  56. package/lib/typescript/components/actions.d.ts.map +1 -0
  57. package/lib/typescript/components/containers.d.ts +59 -0
  58. package/lib/typescript/components/containers.d.ts.map +1 -0
  59. package/lib/typescript/components/display.d.ts +68 -0
  60. package/lib/typescript/components/display.d.ts.map +1 -0
  61. package/lib/typescript/components/feedback.d.ts +96 -0
  62. package/lib/typescript/components/feedback.d.ts.map +1 -0
  63. package/lib/typescript/components/inputs.d.ts +141 -0
  64. package/lib/typescript/components/inputs.d.ts.map +1 -0
  65. package/lib/typescript/components/mobile.d.ts +173 -0
  66. package/lib/typescript/components/mobile.d.ts.map +1 -0
  67. package/lib/typescript/components/overlays.d.ts +74 -0
  68. package/lib/typescript/components/overlays.d.ts.map +1 -0
  69. package/lib/typescript/components/shared.d.ts +20 -0
  70. package/lib/typescript/components/shared.d.ts.map +1 -0
  71. package/lib/typescript/hooks/useControllableState.d.ts +6 -0
  72. package/lib/typescript/hooks/useControllableState.d.ts.map +1 -0
  73. package/lib/typescript/index.d.ts +39 -0
  74. package/lib/typescript/index.d.ts.map +1 -0
  75. package/lib/typescript/package.json +1 -0
  76. package/lib/typescript/patterns/app.d.ts +139 -0
  77. package/lib/typescript/patterns/app.d.ts.map +1 -0
  78. package/lib/typescript/patterns/auth.d.ts +54 -0
  79. package/lib/typescript/patterns/auth.d.ts.map +1 -0
  80. package/lib/typescript/patterns/home.d.ts +50 -0
  81. package/lib/typescript/patterns/home.d.ts.map +1 -0
  82. package/lib/typescript/patterns/index.d.ts +9 -0
  83. package/lib/typescript/patterns/index.d.ts.map +1 -0
  84. package/lib/typescript/patterns/money.d.ts +106 -0
  85. package/lib/typescript/patterns/money.d.ts.map +1 -0
  86. package/lib/typescript/primitives/Icon.d.ts +12 -0
  87. package/lib/typescript/primitives/Icon.d.ts.map +1 -0
  88. package/lib/typescript/primitives/Layout.d.ts +26 -0
  89. package/lib/typescript/primitives/Layout.d.ts.map +1 -0
  90. package/lib/typescript/primitives/Pressable.d.ts +21 -0
  91. package/lib/typescript/primitives/Pressable.d.ts.map +1 -0
  92. package/lib/typescript/primitives/ScreenContainer.d.ts +25 -0
  93. package/lib/typescript/primitives/ScreenContainer.d.ts.map +1 -0
  94. package/lib/typescript/primitives/Surface.d.ts +23 -0
  95. package/lib/typescript/primitives/Surface.d.ts.map +1 -0
  96. package/lib/typescript/primitives/Text.d.ts +21 -0
  97. package/lib/typescript/primitives/Text.d.ts.map +1 -0
  98. package/lib/typescript/theme/ThemeProvider.d.ts +69 -0
  99. package/lib/typescript/theme/ThemeProvider.d.ts.map +1 -0
  100. package/lib/typescript/theme/fonts.d.ts +25 -0
  101. package/lib/typescript/theme/fonts.d.ts.map +1 -0
  102. package/lib/typescript/theme/haptics.d.ts +3 -0
  103. package/lib/typescript/theme/haptics.d.ts.map +1 -0
  104. package/lib/typescript/theme/motion.d.ts +79 -0
  105. package/lib/typescript/theme/motion.d.ts.map +1 -0
  106. package/lib/typescript/theme/navigation.d.ts +158 -0
  107. package/lib/typescript/theme/navigation.d.ts.map +1 -0
  108. package/package.json +138 -3
  109. package/src/components/actions.tsx +259 -0
  110. package/src/components/containers.tsx +352 -0
  111. package/src/components/display.tsx +345 -0
  112. package/src/components/feedback.tsx +541 -0
  113. package/src/components/inputs.tsx +899 -0
  114. package/src/components/mobile.tsx +883 -0
  115. package/src/components/overlays.tsx +488 -0
  116. package/src/components/shared.ts +54 -0
  117. package/src/hooks/useControllableState.ts +27 -0
  118. package/src/index.ts +177 -0
  119. package/src/patterns/app.tsx +715 -0
  120. package/src/patterns/auth.tsx +266 -0
  121. package/src/patterns/home.tsx +211 -0
  122. package/src/patterns/index.ts +59 -0
  123. package/src/patterns/money.tsx +458 -0
  124. package/src/primitives/Icon.tsx +15 -0
  125. package/src/primitives/Layout.tsx +71 -0
  126. package/src/primitives/Pressable.tsx +97 -0
  127. package/src/primitives/ScreenContainer.tsx +65 -0
  128. package/src/primitives/Surface.tsx +75 -0
  129. package/src/primitives/Text.tsx +79 -0
  130. package/src/theme/ThemeProvider.tsx +210 -0
  131. package/src/theme/fonts.ts +52 -0
  132. package/src/theme/haptics.ts +57 -0
  133. package/src/theme/motion.ts +105 -0
  134. package/src/theme/navigation.ts +68 -0
@@ -0,0 +1,818 @@
1
+ "use strict";
2
+
3
+ import * as React from 'react';
4
+ import { AppState, FlatList, KeyboardAvoidingView, PanResponder, Platform, RefreshControl as RNRefreshControl, StyleSheet, TextInput, View, useWindowDimensions } from 'react-native';
5
+ import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
6
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
7
+ import { currencySymbol, fractionDigits, parseMoney } from '@nexub/core';
8
+ import { Calendar, Delete, Eye, EyeOff } from '@nexub/icons/native';
9
+ import { useControllableState } from "../hooks/useControllableState.js";
10
+ import { Pressable } from "../primitives/Pressable.js";
11
+ import { Surface } from "../primitives/Surface.js";
12
+ import { Text } from "../primitives/Text.js";
13
+ import { fontStyle } from "../theme/fonts.js";
14
+ import { useHaptics, useTheme } from "../theme/ThemeProvider.js";
15
+ import { nxExit, nxLayout, nxSpring } from "../theme/motion.js";
16
+ import { Button, IconButton } from "./actions.js";
17
+ import { Banner } from "./feedback.js";
18
+ import { BottomSheet } from "./overlays.js";
19
+
20
+ // ─── SwipeableRow ────────────────────────────────────────────────────────────
21
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
22
+ const SWIPE_ACTION_WIDTH = 80;
23
+
24
+ /**
25
+ * Swipe a row to reveal actions. Never the only way to an action: they are
26
+ * custom accessibility actions too (VoiceOver rotor / TalkBack actions).
27
+ */
28
+ export function SwipeableRow({
29
+ label,
30
+ onPress,
31
+ actions,
32
+ children,
33
+ style
34
+ }) {
35
+ const {
36
+ roles,
37
+ dir
38
+ } = useTheme();
39
+ const play = useHaptics();
40
+ const width = actions.length * SWIPE_ACTION_WIDTH;
41
+ const sign = dir === 'rtl' ? 1 : -1; // open = content moves toward the start edge
42
+ const x = useSharedValue(0);
43
+ const open = React.useRef(false);
44
+ const settle = to => {
45
+ if (to !== open.current) play('snap');
46
+ open.current = to;
47
+ x.value = withSpring(to ? sign * width : 0, nxSpring('spatial', 'default'));
48
+ };
49
+ const responder = React.useMemo(() => PanResponder.create({
50
+ onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 8 && Math.abs(g.dx) > Math.abs(g.dy) * 1.5,
51
+ onPanResponderMove: (_, g) => {
52
+ const base = open.current ? sign * width : 0;
53
+ const next = base + g.dx;
54
+ x.value = sign < 0 ? Math.min(0, Math.max(-width * 1.2, next)) : Math.max(0, Math.min(width * 1.2, next));
55
+ },
56
+ onPanResponderRelease: (_, g) => settle(Math.abs((open.current ? sign * width : 0) + g.dx) > width / 2),
57
+ onPanResponderTerminate: () => settle(open.current)
58
+ }),
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ [width, sign]);
61
+ const slide = useAnimatedStyle(() => ({
62
+ transform: [{
63
+ translateX: x.value
64
+ }]
65
+ }));
66
+ const bg = {
67
+ neutral: ['surface-container-highest', 'on-surface'],
68
+ danger: ['error', 'on-error'],
69
+ primary: ['primary', 'on-primary']
70
+ };
71
+ return (
72
+ /*#__PURE__*/
73
+ // Removed (e.g. swipe-to-delete): slides out toward the start edge while the
74
+ // rows below glide up. In a FlatList, pass nxLayout() as itemLayoutAnimation.
75
+ _jsxs(Animated.View, {
76
+ exiting: nxExit('start', 'default', dir === 'rtl'),
77
+ layout: nxLayout(),
78
+ style: [{
79
+ overflow: 'hidden'
80
+ }, style]
81
+ // One screen-reader element for the row: its name, its tap, and every swipe action.
82
+ ,
83
+ accessible: true,
84
+ accessibilityRole: onPress ? 'button' : undefined,
85
+ accessibilityLabel: label,
86
+ accessibilityActions: [...(onPress ? [{
87
+ name: 'activate'
88
+ }] : []), ...actions.map(a => ({
89
+ name: a.label,
90
+ label: a.label
91
+ }))],
92
+ onAccessibilityAction: e => {
93
+ const name = e.nativeEvent.actionName;
94
+ if (name === 'activate') onPress?.();else actions.find(a => a.label === name)?.onPress();
95
+ },
96
+ children: [/*#__PURE__*/_jsx(View, {
97
+ style: [StyleSheet.absoluteFill, {
98
+ flexDirection: 'row',
99
+ justifyContent: 'flex-end'
100
+ }],
101
+ importantForAccessibility: "no-hide-descendants",
102
+ accessibilityElementsHidden: true,
103
+ children: actions.map(a => {
104
+ const [b, f] = bg[a.tone ?? 'neutral'];
105
+ const Glyph = a.icon;
106
+ return /*#__PURE__*/_jsxs(Pressable, {
107
+ accessibilityLabel: a.label,
108
+ onPress: () => {
109
+ settle(false);
110
+ a.onPress();
111
+ },
112
+ pressScale: false,
113
+ stateLayer: f,
114
+ style: {
115
+ width: SWIPE_ACTION_WIDTH,
116
+ backgroundColor: roles[b],
117
+ alignItems: 'center',
118
+ justifyContent: 'center',
119
+ gap: 4
120
+ },
121
+ children: [Glyph && /*#__PURE__*/_jsx(Glyph, {
122
+ size: "md",
123
+ color: roles[f]
124
+ }), /*#__PURE__*/_jsx(Text, {
125
+ variant: "label-md",
126
+ color: f,
127
+ children: a.label
128
+ })]
129
+ }, a.label);
130
+ })
131
+ }), /*#__PURE__*/_jsx(Animated.View, {
132
+ style: [{
133
+ backgroundColor: roles['surface-container-lowest']
134
+ }, slide],
135
+ ...responder.panHandlers,
136
+ children: children
137
+ })]
138
+ })
139
+ );
140
+ }
141
+
142
+ // ─── RefreshControl ──────────────────────────────────────────────────────────
143
+
144
+ /** The platform pull-to-refresh in the primary role. Pass to a ScrollView/FlatList `refreshControl`. */
145
+ export function RefreshControl(props) {
146
+ const {
147
+ roles
148
+ } = useTheme();
149
+ return /*#__PURE__*/_jsx(RNRefreshControl, {
150
+ tintColor: roles.primary,
151
+ colors: [roles.primary],
152
+ progressBackgroundColor: roles['surface-container-high'],
153
+ ...props
154
+ });
155
+ }
156
+
157
+ // ─── Carousel + PageIndicator ────────────────────────────────────────────────
158
+
159
+ /** Dots for paged content: "Page 2 of 4" to screen readers. */
160
+ export function PageIndicator({
161
+ count,
162
+ index,
163
+ style
164
+ }) {
165
+ const {
166
+ roles,
167
+ messages
168
+ } = useTheme();
169
+ return /*#__PURE__*/_jsx(View, {
170
+ accessible: true,
171
+ accessibilityLabel: messages.page(index + 1, count),
172
+ style: [{
173
+ flexDirection: 'row',
174
+ gap: 6,
175
+ alignSelf: 'center'
176
+ }, style],
177
+ children: Array.from({
178
+ length: count
179
+ }, (_, i) => /*#__PURE__*/_jsx(View, {
180
+ style: {
181
+ width: i === index ? 20 : 6,
182
+ height: 6,
183
+ borderRadius: 3,
184
+ backgroundColor: roles[i === index ? 'primary' : 'outline-variant']
185
+ }
186
+ }, i))
187
+ });
188
+ }
189
+ /** Full-width paging (onboarding, card stacks) with a page indicator. */
190
+ export function Carousel({
191
+ data,
192
+ renderItem,
193
+ width: w,
194
+ onIndexChange,
195
+ style
196
+ }) {
197
+ const {
198
+ width: window
199
+ } = useWindowDimensions();
200
+ const width = w ?? window;
201
+ const [index, setIndex] = React.useState(0);
202
+ const {
203
+ tokens
204
+ } = useTheme();
205
+ return /*#__PURE__*/_jsxs(View, {
206
+ style: [{
207
+ gap: tokens.spacing[3]
208
+ }, style],
209
+ children: [/*#__PURE__*/_jsx(FlatList, {
210
+ data: data,
211
+ horizontal: true,
212
+ pagingEnabled: true,
213
+ showsHorizontalScrollIndicator: false,
214
+ renderItem: info => /*#__PURE__*/_jsx(View, {
215
+ style: {
216
+ width
217
+ },
218
+ children: renderItem(info)
219
+ }),
220
+ keyExtractor: (_, i) => String(i),
221
+ onMomentumScrollEnd: e => {
222
+ const i = Math.round(e.nativeEvent.contentOffset.x / width);
223
+ setIndex(i);
224
+ onIndexChange?.(i);
225
+ }
226
+ }), /*#__PURE__*/_jsx(PageIndicator, {
227
+ count: data.length,
228
+ index: index
229
+ })]
230
+ });
231
+ }
232
+
233
+ // ─── StickyFooter ────────────────────────────────────────────────────────────
234
+
235
+ /** Screen body + a footer that stays above the keyboard: the primary button of every flow. */
236
+ export function StickyFooter({
237
+ children,
238
+ footer,
239
+ style
240
+ }) {
241
+ const {
242
+ roles,
243
+ tokens
244
+ } = useTheme();
245
+ const insets = useSafeAreaInsets();
246
+ return /*#__PURE__*/_jsxs(KeyboardAvoidingView, {
247
+ behavior: Platform.OS === 'ios' ? 'padding' : 'height',
248
+ style: [{
249
+ flex: 1,
250
+ backgroundColor: roles.surface
251
+ }, style],
252
+ children: [/*#__PURE__*/_jsx(View, {
253
+ style: {
254
+ flex: 1
255
+ },
256
+ children: children
257
+ }), /*#__PURE__*/_jsx(View, {
258
+ style: {
259
+ paddingHorizontal: tokens.spacing[4],
260
+ paddingTop: tokens.spacing[3],
261
+ paddingBottom: Math.max(insets.bottom, tokens.spacing[4]),
262
+ gap: tokens.spacing[2],
263
+ backgroundColor: roles.surface,
264
+ borderTopWidth: StyleSheet.hairlineWidth,
265
+ borderTopColor: roles['outline-variant']
266
+ },
267
+ children: footer
268
+ })]
269
+ });
270
+ }
271
+
272
+ // ─── OTPInput ────────────────────────────────────────────────────────────────
273
+
274
+ /**
275
+ * One-time code / PIN entry: one hidden field (so paste and SMS autofill work:
276
+ * iOS oneTimeCode, Android sms-otp) drawn as boxes.
277
+ */
278
+ export function OTPInput({
279
+ label,
280
+ length = 6,
281
+ value,
282
+ onValueChange,
283
+ onComplete,
284
+ secure,
285
+ error,
286
+ autoFocus,
287
+ style
288
+ }) {
289
+ const {
290
+ roles,
291
+ tokens,
292
+ fonts
293
+ } = useTheme();
294
+ const ref = React.useRef(null);
295
+ const [focused, setFocused] = React.useState(false);
296
+ const [code, setCode] = useControllableState({
297
+ value,
298
+ defaultValue: '',
299
+ onChange: onValueChange
300
+ });
301
+ return /*#__PURE__*/_jsxs(View, {
302
+ style: [{
303
+ gap: tokens.spacing[2]
304
+ }, style],
305
+ children: [/*#__PURE__*/_jsx(Pressable, {
306
+ onPress: () => ref.current?.focus(),
307
+ stateLayer: false,
308
+ pressScale: false,
309
+ accessible: false,
310
+ style: {
311
+ flexDirection: 'row',
312
+ gap: tokens.spacing[2],
313
+ justifyContent: 'center'
314
+ },
315
+ children: Array.from({
316
+ length
317
+ }, (_, i) => {
318
+ const ch = code[i];
319
+ const active = focused && i === Math.min(code.length, length - 1);
320
+ return /*#__PURE__*/_jsx(View, {
321
+ style: {
322
+ width: 44,
323
+ height: 52,
324
+ borderRadius: tokens.shape.field,
325
+ borderWidth: active || error ? 2 : 1,
326
+ borderColor: error ? roles.error : active ? roles.primary : roles.outline,
327
+ backgroundColor: roles['surface-container-lowest'],
328
+ alignItems: 'center',
329
+ justifyContent: 'center'
330
+ },
331
+ children: /*#__PURE__*/_jsx(Text, {
332
+ variant: "title-lg",
333
+ tabular: true,
334
+ children: ch ? secure ? '•' : ch : ''
335
+ })
336
+ }, i);
337
+ })
338
+ }), /*#__PURE__*/_jsx(TextInput, {
339
+ ref: ref,
340
+ accessibilityLabel: label,
341
+ accessibilityHint: error,
342
+ value: code,
343
+ onChangeText: t => {
344
+ const next = t.replace(/\D/g, '').slice(0, length);
345
+ setCode(next);
346
+ if (next.length === length) onComplete?.(next);
347
+ },
348
+ onFocus: () => setFocused(true),
349
+ onBlur: () => setFocused(false),
350
+ autoFocus: autoFocus,
351
+ keyboardType: "number-pad",
352
+ inputMode: "numeric",
353
+ maxLength: length,
354
+ secureTextEntry: secure,
355
+ textContentType: secure ? 'password' : 'oneTimeCode',
356
+ autoComplete: secure ? 'off' : 'sms-otp',
357
+ caretHidden: true,
358
+ style: {
359
+ position: 'absolute',
360
+ opacity: 0.011,
361
+ width: 1,
362
+ height: 1,
363
+ ...fontStyle('sans', 400, fonts)
364
+ }
365
+ }), error && /*#__PURE__*/_jsx(Text, {
366
+ variant: "body-sm",
367
+ color: "error",
368
+ style: {
369
+ textAlign: 'center'
370
+ },
371
+ accessibilityLiveRegion: "polite",
372
+ children: error
373
+ })]
374
+ });
375
+ }
376
+
377
+ // ─── NumericKeypad + ExpressiveMoneyInput ────────────────────────────────────
378
+
379
+ /** On-screen digits for amounts and PINs (no system keyboard covering the screen). */
380
+ export function NumericKeypad({
381
+ onKey,
382
+ decimal,
383
+ locale,
384
+ style
385
+ }) {
386
+ const {
387
+ roles,
388
+ messages,
389
+ locale: loc
390
+ } = useTheme();
391
+ const sep = new Intl.NumberFormat(locale ?? loc).format(1.5).replace(/\p{Nd}/gu, '') || '.';
392
+ const keys = ['1', '2', '3', '4', '5', '6', '7', '8', '9', decimal ? sep : '', '0', 'delete'];
393
+ return /*#__PURE__*/_jsx(View, {
394
+ style: [{
395
+ flexDirection: 'row',
396
+ flexWrap: 'wrap'
397
+ }, style],
398
+ children: keys.map((k, i) => k === '' ? /*#__PURE__*/_jsx(View, {
399
+ style: {
400
+ width: '33.333%',
401
+ height: 64
402
+ }
403
+ }, i) : /*#__PURE__*/_jsx(Pressable, {
404
+ accessibilityLabel: k === 'delete' ? messages.deleteDigit : k,
405
+ haptic: "selection",
406
+ onPress: () => onKey(k),
407
+ style: {
408
+ width: '33.333%',
409
+ height: 64,
410
+ alignItems: 'center',
411
+ justifyContent: 'center',
412
+ borderRadius: 16
413
+ },
414
+ children: k === 'delete' ? /*#__PURE__*/_jsx(Delete, {
415
+ size: "lg",
416
+ color: roles['on-surface']
417
+ }) : /*#__PURE__*/_jsx(Text, {
418
+ variant: "headline-md",
419
+ children: k
420
+ })
421
+ }, i))
422
+ });
423
+ }
424
+ /** Big amount in the brand face + keypad (Wise-style money entry). */
425
+ export function ExpressiveMoneyInput({
426
+ currency,
427
+ value,
428
+ onValueChange,
429
+ label,
430
+ locale,
431
+ caption,
432
+ error,
433
+ style
434
+ }) {
435
+ const theme = useTheme();
436
+ const loc = locale ?? theme.locale;
437
+ const digits = fractionDigits(currency, loc);
438
+ const sep = new Intl.NumberFormat(loc).format(1.5).replace(/\p{Nd}/gu, '') || '.';
439
+ const [text, setText] = React.useState(value == null ? '' : String(value).replace('.', sep));
440
+ const press = k => {
441
+ let next = text;
442
+ if (k === 'delete') next = text.slice(0, -1);else if (k === sep) next = digits === 0 || text.includes(sep) ? text : (text || '0') + sep;else {
443
+ const [, frac] = text.split(sep);
444
+ if (frac !== undefined && frac.length >= digits) return;
445
+ next = text === '0' ? k : text + k;
446
+ }
447
+ setText(next);
448
+ onValueChange?.(next ? parseMoney(next, currency, loc) : null);
449
+ };
450
+ return /*#__PURE__*/_jsxs(View, {
451
+ style: [{
452
+ gap: theme.tokens.spacing[4]
453
+ }, style],
454
+ children: [/*#__PURE__*/_jsxs(View, {
455
+ accessible: true,
456
+ accessibilityLabel: `${label}, ${text || '0'} ${currency}`,
457
+ accessibilityLiveRegion: "polite",
458
+ style: {
459
+ alignItems: 'center',
460
+ gap: 4
461
+ },
462
+ children: [/*#__PURE__*/_jsxs(Text, {
463
+ variant: "display-md",
464
+ color: error ? 'error' : 'on-surface',
465
+ tabular: true,
466
+ children: [/*#__PURE__*/_jsx(Text, {
467
+ variant: "headline-md",
468
+ color: "on-surface-variant",
469
+ children: currencySymbol(currency, loc)
470
+ }), text || '0']
471
+ }), caption && /*#__PURE__*/_jsx(Text, {
472
+ variant: "body-md",
473
+ color: error ? 'error' : 'on-surface-variant',
474
+ children: caption
475
+ })]
476
+ }), /*#__PURE__*/_jsx(NumericKeypad, {
477
+ onKey: press,
478
+ decimal: digits > 0,
479
+ locale: loc
480
+ })]
481
+ });
482
+ }
483
+
484
+ // ─── SensitiveValue ──────────────────────────────────────────────────────────
485
+
486
+ /**
487
+ * A value that can be hidden (balances, card numbers). Masked text reads as
488
+ * "Balance, hidden", never as dots, and it re-masks when the app backgrounds.
489
+ */
490
+ export function SensitiveValue({
491
+ label,
492
+ children,
493
+ variant = 'title-lg',
494
+ revealed,
495
+ defaultRevealed = false,
496
+ onRevealedChange
497
+ }) {
498
+ const {
499
+ messages
500
+ } = useTheme();
501
+ const [shown, setShown] = useControllableState({
502
+ value: revealed,
503
+ defaultValue: defaultRevealed,
504
+ onChange: onRevealedChange
505
+ });
506
+ React.useEffect(() => {
507
+ const sub = AppState.addEventListener('change', s => s !== 'active' && setShown(false));
508
+ return () => sub.remove();
509
+ }, [setShown]);
510
+ return /*#__PURE__*/_jsxs(View, {
511
+ style: {
512
+ flexDirection: 'row',
513
+ alignItems: 'center',
514
+ gap: 4
515
+ },
516
+ children: [/*#__PURE__*/_jsx(Text, {
517
+ variant: variant,
518
+ tabular: true,
519
+ accessibilityLabel: `${label}, ${shown ? children : messages.hidden}`,
520
+ children: shown ? children : '••••••'
521
+ }), /*#__PURE__*/_jsx(IconButton, {
522
+ icon: shown ? EyeOff : Eye,
523
+ label: shown ? messages.hide(label) : messages.show(label),
524
+ size: "sm",
525
+ onPress: () => setShown(!shown)
526
+ })]
527
+ });
528
+ }
529
+
530
+ // ─── PrivacyShield ───────────────────────────────────────────────────────────
531
+
532
+ let ScreenCapture;
533
+ try {
534
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- optional peer, must not fail when absent
535
+ ScreenCapture = require('expo-screen-capture');
536
+ } catch {
537
+ ScreenCapture = undefined;
538
+ }
539
+ /**
540
+ * Covers the app when it leaves the foreground, so the app-switcher snapshot
541
+ * shows no balances. Wrap screens with sensitive data (or the whole app).
542
+ */
543
+ export function PrivacyShield({
544
+ children,
545
+ cover,
546
+ preventCapture
547
+ }) {
548
+ const {
549
+ roles
550
+ } = useTheme();
551
+ const [covered, setCovered] = React.useState(false);
552
+ React.useEffect(() => {
553
+ const sub = AppState.addEventListener('change', s => setCovered(s !== 'active'));
554
+ return () => sub.remove();
555
+ }, []);
556
+ React.useEffect(() => {
557
+ if (!preventCapture || !ScreenCapture) return;
558
+ const key = 'nexub-privacy-shield';
559
+ ScreenCapture.preventScreenCaptureAsync(key).catch(() => {});
560
+ return () => {
561
+ ScreenCapture?.allowScreenCaptureAsync(key).catch(() => {});
562
+ };
563
+ }, [preventCapture]);
564
+ return /*#__PURE__*/_jsxs(View, {
565
+ style: {
566
+ flex: 1
567
+ },
568
+ children: [children, covered && /*#__PURE__*/_jsx(View, {
569
+ style: [StyleSheet.absoluteFill, {
570
+ backgroundColor: roles.surface,
571
+ alignItems: 'center',
572
+ justifyContent: 'center'
573
+ }],
574
+ accessibilityElementsHidden: true,
575
+ children: cover
576
+ })]
577
+ });
578
+ }
579
+
580
+ // ─── OfflineBanner ───────────────────────────────────────────────────────────
581
+
582
+ /** Slim notice while the app has no connection. Pass your network state (e.g. NetInfo). */
583
+ export function OfflineBanner({
584
+ offline,
585
+ style
586
+ }) {
587
+ const {
588
+ messages
589
+ } = useTheme();
590
+ if (!offline) return null;
591
+ return /*#__PURE__*/_jsx(Banner, {
592
+ tone: "warning",
593
+ style: [{
594
+ borderRadius: 0,
595
+ paddingVertical: 10
596
+ }, style],
597
+ accessibilityLiveRegion: "polite",
598
+ children: messages.offline
599
+ });
600
+ }
601
+
602
+ // ─── Coachmark ───────────────────────────────────────────────────────────────
603
+
604
+ /** A one-time hint next to a new feature. Place it in your layout; it doesn't measure targets. */
605
+ export function Coachmark({
606
+ title,
607
+ children,
608
+ arrow = 'top',
609
+ onDismiss,
610
+ dismissLabel,
611
+ style
612
+ }) {
613
+ const {
614
+ roles,
615
+ tokens,
616
+ messages
617
+ } = useTheme();
618
+ const tip = /*#__PURE__*/_jsx(View, {
619
+ style: {
620
+ width: 14,
621
+ height: 14,
622
+ marginStart: 24,
623
+ transform: [{
624
+ rotate: '45deg'
625
+ }],
626
+ backgroundColor: roles['inverse-surface'],
627
+ marginVertical: -7
628
+ }
629
+ });
630
+ return /*#__PURE__*/_jsxs(View, {
631
+ accessibilityRole: "alert",
632
+ style: style,
633
+ children: [arrow === 'top' && tip, /*#__PURE__*/_jsxs(View, {
634
+ style: {
635
+ padding: tokens.spacing[4],
636
+ gap: tokens.spacing[2],
637
+ borderRadius: tokens.shape.container,
638
+ backgroundColor: roles['inverse-surface']
639
+ },
640
+ children: [title && /*#__PURE__*/_jsx(Text, {
641
+ variant: "title-sm",
642
+ color: "inverse-on-surface",
643
+ children: title
644
+ }), /*#__PURE__*/_jsx(Text, {
645
+ variant: "body-sm",
646
+ color: "inverse-on-surface",
647
+ children: children
648
+ }), /*#__PURE__*/_jsx(Button, {
649
+ variant: "text",
650
+ size: "sm",
651
+ onPress: onDismiss,
652
+ style: {
653
+ alignSelf: 'flex-end'
654
+ },
655
+ children: dismissLabel ?? messages.close
656
+ })]
657
+ }), arrow === 'bottom' && tip]
658
+ });
659
+ }
660
+
661
+ // ─── FAB ─────────────────────────────────────────────────────────────────────
662
+
663
+ /** Floating primary action, bottom end. Android only by default (MOBILE_PLAN.md D7). */
664
+ export function FAB({
665
+ icon: Glyph,
666
+ label,
667
+ onPress,
668
+ extended,
669
+ platforms = ['android'],
670
+ bottomOffset = 0
671
+ }) {
672
+ const {
673
+ roles,
674
+ tokens
675
+ } = useTheme();
676
+ const insets = useSafeAreaInsets();
677
+ if (!platforms.includes(Platform.OS)) return null;
678
+ return /*#__PURE__*/_jsxs(Pressable, {
679
+ accessibilityLabel: label,
680
+ onPress: onPress,
681
+ stateLayer: "on-primary-container",
682
+ style: {
683
+ position: 'absolute',
684
+ end: 16,
685
+ bottom: insets.bottom + 16 + bottomOffset,
686
+ height: 56,
687
+ minWidth: 56,
688
+ paddingHorizontal: extended ? 20 : 0,
689
+ borderRadius: 16,
690
+ flexDirection: 'row',
691
+ alignItems: 'center',
692
+ justifyContent: 'center',
693
+ gap: 12,
694
+ backgroundColor: roles['primary-container'],
695
+ boxShadow: tokens.elevation[3].light
696
+ },
697
+ children: [/*#__PURE__*/_jsx(Glyph, {
698
+ size: "lg",
699
+ color: roles['on-primary-container']
700
+ }), extended && /*#__PURE__*/_jsx(Text, {
701
+ variant: "label-lg",
702
+ color: "on-primary-container",
703
+ children: label
704
+ })]
705
+ });
706
+ }
707
+
708
+ // ─── DateTimeField ───────────────────────────────────────────────────────────
709
+
710
+ let Picker;
711
+ try {
712
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- optional peer, must not fail when absent
713
+ Picker = require('@react-native-community/datetimepicker');
714
+ } catch {
715
+ Picker = undefined;
716
+ }
717
+ /**
718
+ * A field that opens the platform picker (Android dialog, iOS inline calendar
719
+ * in a sheet). Needs @react-native-community/datetimepicker.
720
+ */
721
+ export function DateTimeField({
722
+ label,
723
+ value,
724
+ onValueChange,
725
+ mode = 'date',
726
+ minimumDate,
727
+ maximumDate,
728
+ placeholder,
729
+ style
730
+ }) {
731
+ const theme = useTheme();
732
+ const {
733
+ roles,
734
+ tokens,
735
+ messages,
736
+ locale
737
+ } = theme;
738
+ const [open, setOpen] = React.useState(false);
739
+ if (!Picker) throw new Error('@nexub/react-native: DateTimeField needs @react-native-community/datetimepicker (npx expo install @react-native-community/datetimepicker).');
740
+ const {
741
+ default: RNDateTimePicker,
742
+ DateTimePickerAndroid
743
+ } = Picker;
744
+ const text = value ? new Intl.DateTimeFormat(locale, mode === 'date' ? {
745
+ dateStyle: 'medium'
746
+ } : {
747
+ timeStyle: 'short'
748
+ }).format(value) : placeholder ?? messages.pickDate;
749
+ const show = () => {
750
+ if (Platform.OS === 'android') {
751
+ DateTimePickerAndroid.open({
752
+ value: value ?? new Date(),
753
+ mode,
754
+ minimumDate,
755
+ maximumDate,
756
+ onChange: (e, d) => e.type === 'set' && d && onValueChange(d)
757
+ });
758
+ } else setOpen(true);
759
+ };
760
+ return /*#__PURE__*/_jsxs(View, {
761
+ style: [{
762
+ gap: 6
763
+ }, style],
764
+ children: [/*#__PURE__*/_jsx(Text, {
765
+ variant: "label-lg",
766
+ color: "on-surface-variant",
767
+ importantForAccessibility: "no",
768
+ children: label
769
+ }), /*#__PURE__*/_jsxs(Pressable, {
770
+ accessibilityLabel: `${label}, ${text}`,
771
+ onPress: show,
772
+ pressScale: false,
773
+ style: {
774
+ minHeight: tokens.size.control.lg,
775
+ paddingHorizontal: 14,
776
+ flexDirection: 'row',
777
+ alignItems: 'center',
778
+ gap: 8,
779
+ borderRadius: tokens.shape.field,
780
+ borderWidth: 1,
781
+ borderColor: roles.outline,
782
+ backgroundColor: roles['surface-container-lowest']
783
+ },
784
+ children: [/*#__PURE__*/_jsx(Calendar, {
785
+ size: "sm",
786
+ color: roles['on-surface-variant']
787
+ }), /*#__PURE__*/_jsx(Text, {
788
+ variant: "body-lg",
789
+ color: value ? 'on-surface' : 'on-surface-muted',
790
+ children: text
791
+ })]
792
+ }), Platform.OS === 'ios' && /*#__PURE__*/_jsxs(BottomSheet, {
793
+ open: open,
794
+ onOpenChange: setOpen,
795
+ title: label,
796
+ children: [/*#__PURE__*/_jsx(Surface, {
797
+ tone: "surface-elevated",
798
+ children: /*#__PURE__*/_jsx(RNDateTimePicker, {
799
+ value: value ?? new Date(),
800
+ mode: mode,
801
+ display: mode === 'date' ? 'inline' : 'spinner',
802
+ minimumDate: minimumDate,
803
+ maximumDate: maximumDate,
804
+ accentColor: roles.primary,
805
+ themeVariant: theme.scheme,
806
+ locale: locale,
807
+ onChange: (_, d) => d && onValueChange(d)
808
+ })
809
+ }), /*#__PURE__*/_jsx(Button, {
810
+ size: "lg",
811
+ fullWidth: true,
812
+ onPress: () => setOpen(false),
813
+ children: messages.close
814
+ })]
815
+ })]
816
+ });
817
+ }
818
+ //# sourceMappingURL=mobile.js.map