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