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