@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,792 @@
1
+ "use strict";
2
+
3
+ import * as React from 'react';
4
+ import { PanResponder, Switch as RNSwitch, TextInput, View } from 'react-native';
5
+ import { currencySymbol, formatForEditing, parseMoney } from '@nexub/core';
6
+ import { Check, Eye, EyeOff, Minus, Plus, Search, X } from '@nexub/icons/native';
7
+ import Animated, { LayoutAnimationConfig, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
8
+ import { useControllableState } from "../hooks/useControllableState.js";
9
+ import { Pressable } from "../primitives/Pressable.js";
10
+ import { Text } from "../primitives/Text.js";
11
+ import { fontStyle } from "../theme/fonts.js";
12
+ import { useHaptics, useTheme } from "../theme/ThemeProvider.js";
13
+ import { nxEnter, nxExit, nxLayout, nxSpring } from "../theme/motion.js";
14
+ import { IconButton } from "./actions.js";
15
+ import { controlHeight, disabledOpacity, hitSlopFor } from "./shared.js";
16
+
17
+ // ─── TextField ───────────────────────────────────────────────────────────────
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
+ /** Labelled text input (web Input + Label + HelperText + FormError in one). */
20
+ export function TextField({
21
+ ref,
22
+ label,
23
+ helperText,
24
+ error,
25
+ disabled,
26
+ size = 'lg',
27
+ leading,
28
+ trailing,
29
+ style,
30
+ multiline,
31
+ onFocus,
32
+ onBlur,
33
+ ...rest
34
+ }) {
35
+ const theme = useTheme();
36
+ const {
37
+ roles,
38
+ tokens,
39
+ fonts
40
+ } = theme;
41
+ const [focused, setFocused] = React.useState(false);
42
+ const body = tokens.type['body-lg'];
43
+ const border = error ? roles.error : focused ? roles.primary : roles.outline;
44
+ return /*#__PURE__*/_jsxs(View, {
45
+ style: [{
46
+ gap: 6,
47
+ opacity: disabled ? disabledOpacity(theme) : 1
48
+ }, style],
49
+ children: [/*#__PURE__*/_jsx(Text, {
50
+ variant: "label-lg",
51
+ color: error ? 'error' : 'on-surface-variant',
52
+ importantForAccessibility: "no",
53
+ children: label
54
+ }), /*#__PURE__*/_jsxs(View, {
55
+ style: {
56
+ minHeight: controlHeight(theme, size),
57
+ flexDirection: 'row',
58
+ alignItems: multiline ? 'flex-start' : 'center',
59
+ gap: 8,
60
+ paddingHorizontal: 14,
61
+ borderRadius: tokens.shape.field,
62
+ borderWidth: focused || error ? 2 : 1,
63
+ borderColor: border,
64
+ backgroundColor: roles['surface-container-lowest']
65
+ },
66
+ children: [leading, /*#__PURE__*/_jsx(TextInput, {
67
+ ref: ref,
68
+ accessibilityLabel: label,
69
+ accessibilityHint: error ?? helperText,
70
+ "aria-invalid": !!error,
71
+ editable: !disabled,
72
+ multiline: multiline,
73
+ placeholderTextColor: roles['on-surface-muted'],
74
+ selectionColor: roles.primary,
75
+ onFocus: e => {
76
+ setFocused(true);
77
+ onFocus?.(e);
78
+ },
79
+ onBlur: e => {
80
+ setFocused(false);
81
+ onBlur?.(e);
82
+ },
83
+ style: {
84
+ flex: 1,
85
+ ...fontStyle('sans', 400, fonts),
86
+ fontSize: body.size,
87
+ color: roles['on-surface'],
88
+ paddingVertical: multiline ? 12 : 0,
89
+ minHeight: multiline ? 96 : undefined,
90
+ textAlignVertical: multiline ? 'top' : 'center'
91
+ },
92
+ ...rest
93
+ }), trailing]
94
+ }), (error || helperText) && /*#__PURE__*/_jsx(Text, {
95
+ variant: "body-sm",
96
+ color: error ? 'error' : 'on-surface-variant',
97
+ accessibilityLiveRegion: error ? 'polite' : 'none',
98
+ children: error ?? helperText
99
+ })]
100
+ });
101
+ }
102
+ /** Password field with a show/hide toggle and the platform autofill hints. */
103
+ export function PasswordInput(props) {
104
+ const {
105
+ messages
106
+ } = useTheme();
107
+ const [visible, setVisible] = React.useState(false);
108
+ return /*#__PURE__*/_jsx(TextField, {
109
+ secureTextEntry: !visible,
110
+ autoCapitalize: "none",
111
+ autoCorrect: false,
112
+ autoComplete: "current-password",
113
+ textContentType: "password",
114
+ trailing: /*#__PURE__*/_jsx(IconButton, {
115
+ icon: visible ? EyeOff : Eye,
116
+ label: visible ? messages.hidePassword : messages.showPassword,
117
+ size: "sm",
118
+ onPress: () => setVisible(v => !v)
119
+ }),
120
+ ...props
121
+ });
122
+ }
123
+ /** Capsule search input with a clear button. */
124
+ export function SearchField({
125
+ label,
126
+ value,
127
+ defaultValue,
128
+ onChangeText,
129
+ style,
130
+ ...rest
131
+ }) {
132
+ const theme = useTheme();
133
+ const {
134
+ roles,
135
+ tokens,
136
+ fonts,
137
+ messages
138
+ } = theme;
139
+ const [text, setText] = useControllableState({
140
+ value,
141
+ defaultValue: defaultValue ?? '',
142
+ onChange: onChangeText
143
+ });
144
+ return /*#__PURE__*/_jsxs(View, {
145
+ style: [{
146
+ height: controlHeight(theme, 'md'),
147
+ flexDirection: 'row',
148
+ alignItems: 'center',
149
+ gap: 8,
150
+ paddingStart: 14,
151
+ paddingEnd: 4,
152
+ borderRadius: tokens.shape.control,
153
+ backgroundColor: roles['surface-container-high']
154
+ }, style],
155
+ children: [/*#__PURE__*/_jsx(Search, {
156
+ size: "sm",
157
+ color: roles['on-surface-variant']
158
+ }), /*#__PURE__*/_jsx(TextInput, {
159
+ accessibilityLabel: label ?? messages.search,
160
+ accessibilityRole: "search",
161
+ placeholder: messages.searchPlaceholder,
162
+ placeholderTextColor: roles['on-surface-muted'],
163
+ selectionColor: roles.primary,
164
+ returnKeyType: "search",
165
+ value: text,
166
+ onChangeText: setText,
167
+ style: {
168
+ flex: 1,
169
+ ...fontStyle('sans', 400, fonts),
170
+ fontSize: tokens.type['body-md'].size,
171
+ color: roles['on-surface']
172
+ },
173
+ ...rest
174
+ }), !!text && /*#__PURE__*/_jsx(IconButton, {
175
+ icon: X,
176
+ label: messages.clearSearch,
177
+ size: "xs",
178
+ onPress: () => setText('')
179
+ })]
180
+ });
181
+ }
182
+
183
+ // ─── MoneyInput ──────────────────────────────────────────────────────────────
184
+
185
+ /** Amount entry: decimal keypad, currency symbol, locale-aware parsing (de-DE "1.234,5" → 1234.5). */
186
+ export function MoneyInput({
187
+ currency,
188
+ value,
189
+ defaultValue = null,
190
+ onValueChange,
191
+ locale,
192
+ ...rest
193
+ }) {
194
+ const theme = useTheme();
195
+ const loc = locale ?? theme.locale;
196
+ const [amount, setAmount] = useControllableState({
197
+ value,
198
+ defaultValue,
199
+ onChange: onValueChange
200
+ });
201
+ const [text, setText] = React.useState(amount == null ? '' : formatForEditing(amount, currency, loc));
202
+ React.useEffect(() => {
203
+ // Follow outside changes without fighting the user's typing.
204
+ if (amount !== parseMoney(text, currency, loc)) setText(amount == null ? '' : formatForEditing(amount, currency, loc));
205
+ // eslint-disable-next-line react-hooks/exhaustive-deps
206
+ }, [amount, currency, loc]);
207
+ return /*#__PURE__*/_jsx(TextField, {
208
+ keyboardType: "decimal-pad",
209
+ inputMode: "decimal",
210
+ value: text,
211
+ onChangeText: t => {
212
+ setText(t);
213
+ setAmount(parseMoney(t, currency, loc));
214
+ },
215
+ leading: /*#__PURE__*/_jsx(Text, {
216
+ variant: "body-lg",
217
+ color: "on-surface-variant",
218
+ children: currencySymbol(currency, loc)
219
+ }),
220
+ ...rest
221
+ });
222
+ }
223
+
224
+ // ─── Stepper ─────────────────────────────────────────────────────────────────
225
+
226
+ /** − value + for small counts. Adjustable with VoiceOver/TalkBack swipe up/down. */
227
+ export function Stepper({
228
+ label,
229
+ value,
230
+ defaultValue = 0,
231
+ onValueChange,
232
+ min = -Infinity,
233
+ max = Infinity,
234
+ step = 1,
235
+ disabled,
236
+ style
237
+ }) {
238
+ const {
239
+ messages
240
+ } = useTheme();
241
+ const play = useHaptics();
242
+ const [n, setN] = useControllableState({
243
+ value,
244
+ defaultValue,
245
+ onChange: onValueChange
246
+ });
247
+ const set = next => {
248
+ const clamped = Math.min(max, Math.max(min, next));
249
+ if (clamped !== n) {
250
+ play('selection');
251
+ setN(clamped);
252
+ }
253
+ };
254
+ return /*#__PURE__*/_jsxs(View, {
255
+ accessible: true,
256
+ accessibilityRole: "adjustable",
257
+ accessibilityLabel: label,
258
+ accessibilityValue: {
259
+ now: n,
260
+ min: Number.isFinite(min) ? min : undefined,
261
+ max: Number.isFinite(max) ? max : undefined
262
+ },
263
+ accessibilityActions: [{
264
+ name: 'increment'
265
+ }, {
266
+ name: 'decrement'
267
+ }],
268
+ onAccessibilityAction: e => set(n + (e.nativeEvent.actionName === 'increment' ? step : -step)),
269
+ style: [{
270
+ flexDirection: 'row',
271
+ alignItems: 'center',
272
+ gap: 12,
273
+ alignSelf: 'flex-start'
274
+ }, style],
275
+ children: [/*#__PURE__*/_jsx(IconButton, {
276
+ icon: Minus,
277
+ label: messages.decrement,
278
+ variant: "outlined",
279
+ size: "sm",
280
+ disabled: disabled || n <= min,
281
+ onPress: () => set(n - step)
282
+ }), /*#__PURE__*/_jsx(Text, {
283
+ variant: "title-md",
284
+ tabular: true,
285
+ style: {
286
+ minWidth: 32,
287
+ textAlign: 'center'
288
+ },
289
+ children: n
290
+ }), /*#__PURE__*/_jsx(IconButton, {
291
+ icon: Plus,
292
+ label: messages.increment,
293
+ variant: "outlined",
294
+ size: "sm",
295
+ disabled: disabled || n >= max,
296
+ onPress: () => set(n + step)
297
+ })]
298
+ });
299
+ }
300
+
301
+ // ─── Checkbox / Radio ────────────────────────────────────────────────────────
302
+
303
+ /** Checkbox with its label; the whole row is the target. */
304
+ export function Checkbox({
305
+ label,
306
+ checked,
307
+ defaultChecked = false,
308
+ onCheckedChange,
309
+ description,
310
+ disabled,
311
+ style
312
+ }) {
313
+ const theme = useTheme();
314
+ const {
315
+ roles,
316
+ tokens
317
+ } = theme;
318
+ const [on, setOn] = useControllableState({
319
+ value: checked,
320
+ defaultValue: defaultChecked,
321
+ onChange: onCheckedChange
322
+ });
323
+ return /*#__PURE__*/_jsxs(Pressable, {
324
+ accessibilityRole: "checkbox",
325
+ accessibilityLabel: label,
326
+ accessibilityHint: description,
327
+ accessibilityState: {
328
+ checked: on,
329
+ disabled: !!disabled
330
+ },
331
+ disabled: disabled,
332
+ haptic: on ? 'toggleOff' : 'toggleOn',
333
+ onPress: () => setOn(!on),
334
+ stateLayer: false,
335
+ pressScale: false,
336
+ style: [{
337
+ flexDirection: 'row',
338
+ gap: 12,
339
+ alignItems: 'flex-start',
340
+ minHeight: 44,
341
+ paddingVertical: 10,
342
+ opacity: disabled ? disabledOpacity(theme) : 1
343
+ }, style],
344
+ children: [/*#__PURE__*/_jsx(View, {
345
+ style: {
346
+ width: 22,
347
+ height: 22,
348
+ marginTop: 1,
349
+ borderRadius: tokens.shape.min + 2,
350
+ borderWidth: on ? 0 : 2,
351
+ borderColor: roles.outline,
352
+ backgroundColor: on ? roles.primary : 'transparent',
353
+ alignItems: 'center',
354
+ justifyContent: 'center'
355
+ },
356
+ children: on && /*#__PURE__*/_jsx(Check, {
357
+ size: 16,
358
+ strokeWidth: 3,
359
+ color: roles['on-primary']
360
+ })
361
+ }), /*#__PURE__*/_jsxs(View, {
362
+ style: {
363
+ flex: 1,
364
+ gap: 2
365
+ },
366
+ children: [/*#__PURE__*/_jsx(Text, {
367
+ variant: "body-md",
368
+ children: label
369
+ }), description && /*#__PURE__*/_jsx(Text, {
370
+ variant: "body-sm",
371
+ color: "on-surface-variant",
372
+ children: description
373
+ })]
374
+ })]
375
+ });
376
+ }
377
+ const RadioCtx = /*#__PURE__*/React.createContext(null);
378
+ export function RadioGroup({
379
+ label,
380
+ value,
381
+ defaultValue,
382
+ onValueChange,
383
+ disabled,
384
+ children,
385
+ style
386
+ }) {
387
+ const [v, setV] = useControllableState({
388
+ value,
389
+ defaultValue,
390
+ onChange: onValueChange
391
+ });
392
+ return /*#__PURE__*/_jsx(RadioCtx.Provider, {
393
+ value: {
394
+ value: v,
395
+ select: setV,
396
+ disabled
397
+ },
398
+ children: /*#__PURE__*/_jsx(View, {
399
+ accessibilityRole: "radiogroup",
400
+ accessibilityLabel: label,
401
+ style: style,
402
+ children: children
403
+ })
404
+ });
405
+ }
406
+ export function Radio({
407
+ value,
408
+ label,
409
+ description,
410
+ disabled: own
411
+ }) {
412
+ const theme = useTheme();
413
+ const ctx = React.useContext(RadioCtx);
414
+ if (!ctx) throw new Error('@nexub/react-native: <Radio> must be inside <RadioGroup>.');
415
+ const on = ctx.value === value;
416
+ const disabled = own || ctx.disabled;
417
+ return /*#__PURE__*/_jsxs(Pressable, {
418
+ accessibilityRole: "radio",
419
+ accessibilityLabel: label,
420
+ accessibilityHint: description,
421
+ accessibilityState: {
422
+ checked: on,
423
+ disabled: !!disabled
424
+ },
425
+ disabled: disabled,
426
+ haptic: on ? undefined : 'selection',
427
+ onPress: () => ctx.select(value),
428
+ stateLayer: false,
429
+ pressScale: false,
430
+ style: {
431
+ flexDirection: 'row',
432
+ gap: 12,
433
+ alignItems: 'flex-start',
434
+ minHeight: 44,
435
+ paddingVertical: 10,
436
+ opacity: disabled ? disabledOpacity(theme) : 1
437
+ },
438
+ children: [/*#__PURE__*/_jsx(View, {
439
+ style: {
440
+ width: 22,
441
+ height: 22,
442
+ marginTop: 1,
443
+ borderRadius: 11,
444
+ borderWidth: 2,
445
+ borderColor: on ? theme.roles.primary : theme.roles.outline,
446
+ alignItems: 'center',
447
+ justifyContent: 'center'
448
+ },
449
+ children: on && /*#__PURE__*/_jsx(View, {
450
+ style: {
451
+ width: 10,
452
+ height: 10,
453
+ borderRadius: 5,
454
+ backgroundColor: theme.roles.primary
455
+ }
456
+ })
457
+ }), /*#__PURE__*/_jsxs(View, {
458
+ style: {
459
+ flex: 1,
460
+ gap: 2
461
+ },
462
+ children: [/*#__PURE__*/_jsx(Text, {
463
+ variant: "body-md",
464
+ children: label
465
+ }), description && /*#__PURE__*/_jsx(Text, {
466
+ variant: "body-sm",
467
+ color: "on-surface-variant",
468
+ children: description
469
+ })]
470
+ })]
471
+ });
472
+ }
473
+
474
+ // ─── Switch ──────────────────────────────────────────────────────────────────
475
+
476
+ /** The platform switch (UISwitch / Material), coloured with Nexub roles. The OS plays its haptic. */
477
+ export function Switch({
478
+ label,
479
+ checked,
480
+ defaultChecked = false,
481
+ onCheckedChange,
482
+ disabled
483
+ }) {
484
+ const {
485
+ roles
486
+ } = useTheme();
487
+ const [on, setOn] = useControllableState({
488
+ value: checked,
489
+ defaultValue: defaultChecked,
490
+ onChange: onCheckedChange
491
+ });
492
+ return /*#__PURE__*/_jsx(RNSwitch, {
493
+ accessibilityLabel: label,
494
+ value: on,
495
+ onValueChange: setOn,
496
+ disabled: disabled,
497
+ trackColor: {
498
+ false: roles['surface-container-highest'],
499
+ true: roles.primary
500
+ },
501
+ ios_backgroundColor: roles['surface-container-highest'],
502
+ thumbColor: roles['surface-container-lowest']
503
+ });
504
+ }
505
+
506
+ // ─── SegmentedControl ────────────────────────────────────────────────────────
507
+
508
+ /** 2–5 mutually exclusive views of the same content. */
509
+ export function SegmentedControl({
510
+ label,
511
+ options,
512
+ value,
513
+ defaultValue,
514
+ onValueChange,
515
+ style
516
+ }) {
517
+ const theme = useTheme();
518
+ const {
519
+ roles,
520
+ tokens
521
+ } = theme;
522
+ const [v, setV] = useControllableState({
523
+ value,
524
+ defaultValue: defaultValue ?? options[0].value,
525
+ onChange: onValueChange
526
+ });
527
+ // The selected pill slides between segments on the spatial spring. Until the
528
+ // track is measured, the selected segment paints its own background.
529
+ const [trackWidth, setTrackWidth] = React.useState(0);
530
+ const index = Math.max(0, options.findIndex(o => o.value === v));
531
+ const segment = trackWidth > 0 ? (trackWidth - 6) / options.length : 0;
532
+ const offset = useSharedValue(index * segment);
533
+ const measured = React.useRef(false);
534
+ React.useEffect(() => {
535
+ if (segment === 0) return;
536
+ const to = index * segment * (theme.dir === 'rtl' ? -1 : 1);
537
+ // First measure places the pill; later changes slide it.
538
+ offset.value = measured.current ? withSpring(to, nxSpring('spatial', 'default')) : to;
539
+ measured.current = true;
540
+ }, [index, segment, theme.dir, offset]);
541
+ const indicator = useAnimatedStyle(() => ({
542
+ transform: [{
543
+ translateX: offset.value
544
+ }]
545
+ }));
546
+ return /*#__PURE__*/_jsxs(View, {
547
+ accessibilityRole: "tablist",
548
+ accessibilityLabel: label,
549
+ onLayout: e => setTrackWidth(e.nativeEvent.layout.width),
550
+ style: [{
551
+ flexDirection: 'row',
552
+ padding: 3,
553
+ borderRadius: tokens.shape.control,
554
+ backgroundColor: roles['surface-container-high']
555
+ }, style],
556
+ children: [segment > 0 && /*#__PURE__*/_jsx(Animated.View, {
557
+ pointerEvents: "none",
558
+ style: [{
559
+ position: 'absolute',
560
+ top: 3,
561
+ bottom: 3,
562
+ start: 3,
563
+ width: segment,
564
+ borderRadius: tokens.shape.control,
565
+ backgroundColor: roles['surface-container-lowest'],
566
+ boxShadow: theme.elevation(1)
567
+ }, indicator]
568
+ }), options.map(o => {
569
+ const on = o.value === v;
570
+ return /*#__PURE__*/_jsx(Pressable, {
571
+ accessibilityRole: "tab",
572
+ accessibilityLabel: o.label,
573
+ accessibilityState: {
574
+ selected: on
575
+ },
576
+ haptic: on ? undefined : 'selection',
577
+ onPress: () => setV(o.value),
578
+ stateLayer: on ? false : 'on-surface',
579
+ style: [{
580
+ flex: 1,
581
+ height: controlHeight(theme, 'sm') + 2,
582
+ borderRadius: tokens.shape.control,
583
+ alignItems: 'center',
584
+ justifyContent: 'center'
585
+ }, on && segment === 0 && {
586
+ backgroundColor: roles['surface-container-lowest'],
587
+ boxShadow: theme.elevation(1)
588
+ }],
589
+ children: /*#__PURE__*/_jsx(Text, {
590
+ variant: "label-lg",
591
+ color: on ? 'on-surface' : 'on-surface-variant',
592
+ numberOfLines: 1,
593
+ children: o.label
594
+ })
595
+ }, o.value);
596
+ })]
597
+ });
598
+ }
599
+
600
+ // ─── Slider ──────────────────────────────────────────────────────────────────
601
+
602
+ /** Continuous value. Drag anywhere on the track; VoiceOver/TalkBack adjust by `step`. */
603
+ export function Slider({
604
+ label,
605
+ value,
606
+ defaultValue = 0,
607
+ onValueChange,
608
+ min = 0,
609
+ max = 100,
610
+ step = 1,
611
+ disabled,
612
+ formatValue,
613
+ style
614
+ }) {
615
+ const theme = useTheme();
616
+ const {
617
+ roles
618
+ } = theme;
619
+ const [v, setV] = useControllableState({
620
+ value,
621
+ defaultValue,
622
+ onChange: onValueChange
623
+ });
624
+ const width = React.useRef(1);
625
+ const snap = x => {
626
+ const raw = min + Math.min(1, Math.max(0, x / width.current)) * (max - min);
627
+ return Math.min(max, Math.max(min, Math.round(raw / step) * step));
628
+ };
629
+ const current = React.useRef(v);
630
+ current.current = v;
631
+ const responder = React.useMemo(() => PanResponder.create({
632
+ onStartShouldSetPanResponder: () => !disabled,
633
+ onMoveShouldSetPanResponder: () => !disabled,
634
+ onPanResponderTerminationRequest: () => false,
635
+ onPanResponderGrant: e => setV(snap(e.nativeEvent.locationX)),
636
+ onPanResponderMove: e => {
637
+ const next = snap(e.nativeEvent.locationX);
638
+ if (next !== current.current) setV(next);
639
+ }
640
+ }),
641
+ // eslint-disable-next-line react-hooks/exhaustive-deps
642
+ [disabled, min, max, step]);
643
+ const pct = (v - min) / (max - min) * 100;
644
+ return /*#__PURE__*/_jsxs(View, {
645
+ accessible: true,
646
+ accessibilityRole: "adjustable",
647
+ accessibilityLabel: label,
648
+ accessibilityValue: {
649
+ min,
650
+ max,
651
+ now: v,
652
+ text: formatValue?.(v)
653
+ },
654
+ accessibilityActions: [{
655
+ name: 'increment'
656
+ }, {
657
+ name: 'decrement'
658
+ }],
659
+ onAccessibilityAction: e => setV(Math.min(max, Math.max(min, v + (e.nativeEvent.actionName === 'increment' ? step : -step)))),
660
+ onLayout: e => width.current = e.nativeEvent.layout.width || 1,
661
+ style: [{
662
+ height: 44,
663
+ justifyContent: 'center',
664
+ opacity: disabled ? disabledOpacity(theme) : 1
665
+ }, style],
666
+ ...responder.panHandlers,
667
+ children: [/*#__PURE__*/_jsx(View, {
668
+ pointerEvents: "none",
669
+ style: {
670
+ height: 6,
671
+ borderRadius: 3,
672
+ backgroundColor: roles['surface-container-highest']
673
+ },
674
+ children: /*#__PURE__*/_jsx(View, {
675
+ style: {
676
+ width: `${pct}%`,
677
+ height: 6,
678
+ borderRadius: 3,
679
+ backgroundColor: roles.primary
680
+ }
681
+ })
682
+ }), /*#__PURE__*/_jsx(View, {
683
+ pointerEvents: "none",
684
+ style: {
685
+ position: 'absolute',
686
+ start: `${pct}%`,
687
+ marginStart: -12,
688
+ width: 24,
689
+ height: 24,
690
+ borderRadius: 12,
691
+ backgroundColor: roles['surface-container-lowest'],
692
+ boxShadow: theme.elevation(2)
693
+ }
694
+ })]
695
+ });
696
+ }
697
+
698
+ // ─── Chip ────────────────────────────────────────────────────────────────────
699
+
700
+ /** Capsule control: a filter, a choice or an input token. */
701
+ export function Chip({
702
+ label,
703
+ selected,
704
+ onPress,
705
+ onRemove,
706
+ leadingIcon: Lead,
707
+ disabled,
708
+ style
709
+ }) {
710
+ const theme = useTheme();
711
+ const {
712
+ roles,
713
+ tokens,
714
+ messages
715
+ } = theme;
716
+ const height = controlHeight(theme, 'sm');
717
+ const fg = selected ? 'on-secondary-container' : 'on-surface-variant';
718
+ return (
719
+ /*#__PURE__*/
720
+ // Selecting grows the chip for its check: the width change glides.
721
+ _jsxs(Animated.View, {
722
+ layout: nxLayout('fast'),
723
+ style: [{
724
+ flexDirection: 'row',
725
+ alignItems: 'center',
726
+ alignSelf: 'flex-start',
727
+ opacity: disabled ? disabledOpacity(theme) : 1
728
+ }, style],
729
+ children: [/*#__PURE__*/_jsxs(Pressable, {
730
+ accessibilityRole: selected === undefined ? 'button' : 'checkbox',
731
+ accessibilityLabel: label,
732
+ accessibilityState: {
733
+ checked: selected,
734
+ disabled: !!disabled
735
+ },
736
+ disabled: disabled || !onPress,
737
+ haptic: selected === undefined ? undefined : 'selection',
738
+ onPress: onPress,
739
+ hitSlop: hitSlopFor(height),
740
+ stateLayer: fg,
741
+ style: {
742
+ height,
743
+ paddingStart: 12,
744
+ paddingEnd: onRemove ? 32 : 12,
745
+ borderRadius: tokens.shape.control,
746
+ flexDirection: 'row',
747
+ alignItems: 'center',
748
+ gap: 6,
749
+ borderWidth: selected ? 0 : 1,
750
+ borderColor: roles['outline-variant'],
751
+ backgroundColor: selected ? roles['secondary-container'] : 'transparent'
752
+ },
753
+ children: [/*#__PURE__*/_jsx(LayoutAnimationConfig, {
754
+ skipEntering: true,
755
+ children: selected ? /*#__PURE__*/_jsx(Animated.View, {
756
+ entering: nxEnter('scale', 'fast'),
757
+ exiting: nxExit('scale'),
758
+ children: /*#__PURE__*/_jsx(Check, {
759
+ size: "sm",
760
+ color: roles[fg]
761
+ })
762
+ }, "check") : Lead && /*#__PURE__*/_jsx(Lead, {
763
+ size: "sm",
764
+ color: roles[fg]
765
+ })
766
+ }), /*#__PURE__*/_jsx(Text, {
767
+ variant: "label-lg",
768
+ color: fg,
769
+ children: label
770
+ })]
771
+ }), onRemove && /*#__PURE__*/_jsx(IconButton, {
772
+ icon: X,
773
+ label: `${messages.remove} ${label}`,
774
+ size: "xs",
775
+ onPress: onRemove,
776
+ style: {
777
+ position: 'absolute',
778
+ end: 4,
779
+ top: (height - controlHeight(theme, 'xs')) / 2
780
+ }
781
+ })]
782
+ })
783
+ );
784
+ }
785
+
786
+ /** A toggleable Chip (selected → check icon). */
787
+ export function FilterChip(props) {
788
+ return /*#__PURE__*/_jsx(Chip, {
789
+ ...props
790
+ });
791
+ }
792
+ //# sourceMappingURL=inputs.js.map