panelui-native 0.21.0 → 0.22.3
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.
- package/lib/module/components/input/index.js +15 -5
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/number-input/index.js +350 -0
- package/lib/module/components/number-input/index.js.map +1 -0
- package/lib/module/components/switch/index.js +7 -2
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/icons/index.js +10 -1
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/number-input/index.d.ts +169 -0
- package/lib/typescript/src/components/number-input/index.d.ts.map +1 -0
- package/lib/typescript/src/components/switch/index.d.ts +7 -0
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/icons/index.d.ts +8 -1
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/input/index.tsx +15 -5
- package/src/components/number-input/index.tsx +373 -0
- package/src/components/switch/index.tsx +16 -2
- package/src/icons/index.tsx +9 -2
- package/src/index.ts +4 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NumberInput — a value stepped by two buttons or typed by hand.
|
|
3
|
+
*
|
|
4
|
+
* A number is not a slider: past a handful of steps, dragging to 47 is worse
|
|
5
|
+
* than tapping + a few times or just typing it. So the control keeps both
|
|
6
|
+
* doors open — a − and a + at the ends for nudging, and an editable field in
|
|
7
|
+
* the middle for jumping straight to a value — and reconciles them through a
|
|
8
|
+
* single clamped, step-snapped number.
|
|
9
|
+
*
|
|
10
|
+
* The two ends are not just tap targets. Hold one down and it repeats, after a
|
|
11
|
+
* short delay and then faster, the way a native stepper does — so walking from
|
|
12
|
+
* 0 to 200 is a press, not two hundred of them. The repeat reads the live
|
|
13
|
+
* value from a ref rather than a closure, so it never strides off a stale
|
|
14
|
+
* number.
|
|
15
|
+
*
|
|
16
|
+
* Typing is allowed to be briefly invalid — an empty field, a lone `-`, a
|
|
17
|
+
* trailing `.` — because clamping every keystroke fights the person mid-number.
|
|
18
|
+
* The field commits (parses, snaps, clamps) on blur, and falls back to the
|
|
19
|
+
* last good value when what is left cannot be read as a number.
|
|
20
|
+
*
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <NumberInput defaultValue={1} min={0} max={10} />
|
|
23
|
+
* <NumberInput label="Quantity" value={qty} onValueChange={setQty} />
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Runs controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`).
|
|
27
|
+
*/
|
|
28
|
+
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
|
29
|
+
import { TextInput, View, type TextInputProps, type TextStyle } from 'react-native';
|
|
30
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
31
|
+
import { useCSSVariable } from 'uniwind';
|
|
32
|
+
import { MinusIcon, PlusIcon } from '../../icons';
|
|
33
|
+
import { AnimatedPressable } from '../../primitives/animated-pressable';
|
|
34
|
+
import { Text } from '../../primitives/text';
|
|
35
|
+
import { selectionTick } from '../../utils/haptics';
|
|
36
|
+
import { Label } from '../label';
|
|
37
|
+
|
|
38
|
+
/** Wait before a held button starts repeating — a tap must not trip it. */
|
|
39
|
+
const REPEAT_DELAY = 400;
|
|
40
|
+
/** Interval between repeats once holding — fast enough to cover ground. */
|
|
41
|
+
const REPEAT_INTERVAL = 80;
|
|
42
|
+
|
|
43
|
+
const numberInputVariants = tv({
|
|
44
|
+
slots: {
|
|
45
|
+
container: 'w-full gap-1.5',
|
|
46
|
+
control: 'w-full flex-row items-center overflow-hidden rounded-lg border',
|
|
47
|
+
button: 'h-full items-center justify-center',
|
|
48
|
+
// The field takes the full height so the digits centre against the same
|
|
49
|
+
// box the two glyphs do, rather than against their own line's height.
|
|
50
|
+
field: 'h-full flex-1 text-center font-medium text-foreground',
|
|
51
|
+
description: 'text-sm text-muted-foreground',
|
|
52
|
+
error: 'text-sm text-destructive',
|
|
53
|
+
},
|
|
54
|
+
variants: {
|
|
55
|
+
variant: {
|
|
56
|
+
// Matches Input: outline draws its own edge on the page; filled sits
|
|
57
|
+
// inside a card, where a second border reads as a seam.
|
|
58
|
+
outline: { control: 'border-input bg-background' },
|
|
59
|
+
filled: { control: 'border-transparent bg-muted' },
|
|
60
|
+
},
|
|
61
|
+
size: {
|
|
62
|
+
sm: { control: 'h-10', button: 'w-10' },
|
|
63
|
+
md: { control: 'h-12', button: 'w-12' },
|
|
64
|
+
lg: { control: 'h-14', button: 'w-14' },
|
|
65
|
+
},
|
|
66
|
+
invalid: {
|
|
67
|
+
true: { control: 'border-destructive' },
|
|
68
|
+
},
|
|
69
|
+
disabled: {
|
|
70
|
+
true: { container: 'opacity-[0.64]' },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
defaultVariants: {
|
|
74
|
+
variant: 'outline',
|
|
75
|
+
size: 'md',
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/** Icon size per control size — the glyph scales with its target. */
|
|
80
|
+
const ICON_SIZE: Record<'sm' | 'md' | 'lg', number> = { sm: 16, md: 18, lg: 20 };
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The field's job is not to be a box of its own: it has to put the number on
|
|
84
|
+
* exactly the axis the − and the + sit on, and in the exact middle between
|
|
85
|
+
* them. An editable field does not do that unhelped — it arrives with padding
|
|
86
|
+
* and, on Android, with font padding above and below the line, all of which
|
|
87
|
+
* shift the digits off centre. Zeroing both and centring the line in the full
|
|
88
|
+
* height leaves geometry as the only thing placing the number.
|
|
89
|
+
*
|
|
90
|
+
* Tabular figures on top of that, so the digits keep the same advance widths
|
|
91
|
+
* while the value counts — a 1 that is narrower than a 7 makes a centred
|
|
92
|
+
* number appear to shuffle sideways as it steps.
|
|
93
|
+
*/
|
|
94
|
+
const FIELD_BASE: TextStyle = {
|
|
95
|
+
paddingVertical: 0,
|
|
96
|
+
paddingHorizontal: 0,
|
|
97
|
+
includeFontPadding: false,
|
|
98
|
+
textAlignVertical: 'center',
|
|
99
|
+
fontVariant: ['tabular-nums'],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Font size is set here rather than by a text class because those bring a line
|
|
104
|
+
* height with them: a 16px number in a 24px line box. The extra leading is
|
|
105
|
+
* added above the digits, so centring that box leaves the number sitting a
|
|
106
|
+
* couple of pixels below the − and the + it is meant to line up with — close
|
|
107
|
+
* enough to look like a mistake rather than a difference. Sized this way the
|
|
108
|
+
* line box is the font's own, and centring it centres the digits.
|
|
109
|
+
*/
|
|
110
|
+
const FIELD_STYLE: Record<'sm' | 'md' | 'lg', TextStyle> = {
|
|
111
|
+
sm: { ...FIELD_BASE, fontSize: 14 },
|
|
112
|
+
md: { ...FIELD_BASE, fontSize: 16 },
|
|
113
|
+
lg: { ...FIELD_BASE, fontSize: 16 },
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
type NumberInputVariantProps = VariantProps<typeof numberInputVariants>;
|
|
117
|
+
|
|
118
|
+
export interface NumberInputProps
|
|
119
|
+
extends Omit<NumberInputVariantProps, 'invalid' | 'disabled'>,
|
|
120
|
+
Pick<TextInputProps, 'onFocus' | 'onBlur' | 'placeholder' | 'returnKeyType'> {
|
|
121
|
+
className?: string;
|
|
122
|
+
containerClassName?: string;
|
|
123
|
+
/** Controlled value. Leave unset and pass `defaultValue` to run uncontrolled. */
|
|
124
|
+
value?: number;
|
|
125
|
+
/** Starting value when uncontrolled. Defaults to `min`, or 0. */
|
|
126
|
+
defaultValue?: number;
|
|
127
|
+
/** Lower bound. The decrement button disables here. */
|
|
128
|
+
min?: number;
|
|
129
|
+
/** Upper bound. The increment button disables here. */
|
|
130
|
+
max?: number;
|
|
131
|
+
/** Nudge per press, and the granularity the value snaps to. */
|
|
132
|
+
step?: number;
|
|
133
|
+
/** Fires whenever the committed value changes. */
|
|
134
|
+
onValueChange?: (value: number) => void;
|
|
135
|
+
/**
|
|
136
|
+
* Format the displayed number — units, currency, grouping. The field shows
|
|
137
|
+
* this string; typing still reads a bare number back. Defaults to the value
|
|
138
|
+
* rounded to `step`'s precision.
|
|
139
|
+
*/
|
|
140
|
+
formatValue?: (value: number) => string;
|
|
141
|
+
/** Let the middle field be typed into. When false it is display-only. */
|
|
142
|
+
editable?: boolean;
|
|
143
|
+
disabled?: boolean;
|
|
144
|
+
/** A label above the control. Doubles as the accessibility label. */
|
|
145
|
+
label?: string;
|
|
146
|
+
/** Helper text below the control. Hidden while an error shows. */
|
|
147
|
+
description?: string;
|
|
148
|
+
/** Error message. When set, the control renders in its invalid state. */
|
|
149
|
+
errorMessage?: string;
|
|
150
|
+
/** Marks the field required — an asterisk on the label, and the a11y state. */
|
|
151
|
+
isRequired?: boolean;
|
|
152
|
+
/**
|
|
153
|
+
* Tick the haptic engine on each step. Needs the optional `expo-haptics`,
|
|
154
|
+
* and is silent without it.
|
|
155
|
+
*/
|
|
156
|
+
haptics?: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Decimals implied by `step`, so display and math round to the same place. */
|
|
160
|
+
function precisionOf(step: number): number {
|
|
161
|
+
if (!isFinite(step)) return 0;
|
|
162
|
+
const s = String(step);
|
|
163
|
+
const dot = s.indexOf('.');
|
|
164
|
+
return dot === -1 ? 0 : s.length - dot - 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function clamp(value: number, min: number, max: number): number {
|
|
168
|
+
return Math.min(Math.max(value, min), max);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Snaps to the nearest step from a finite base (or 0 when `min` is open), then
|
|
173
|
+
* rounds to the step's precision so 0.1 + 0.2 does not drift to 0.30000004.
|
|
174
|
+
*/
|
|
175
|
+
function normalize(value: number, min: number, max: number, step: number): number {
|
|
176
|
+
const base = isFinite(min) ? min : 0;
|
|
177
|
+
const snapped = isFinite(step) && step > 0
|
|
178
|
+
? Math.round((value - base) / step) * step + base
|
|
179
|
+
: value;
|
|
180
|
+
const p = precisionOf(step);
|
|
181
|
+
const factor = 10 ** p;
|
|
182
|
+
return clamp(Math.round(snapped * factor) / factor, min, max);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* A number stepped by buttons or typed by hand, reconciled to one clamped,
|
|
187
|
+
* step-snapped value. See the file header for why it keeps both affordances.
|
|
188
|
+
*/
|
|
189
|
+
export const NumberInput = forwardRef<TextInput, NumberInputProps>(
|
|
190
|
+
(
|
|
191
|
+
{
|
|
192
|
+
className,
|
|
193
|
+
containerClassName,
|
|
194
|
+
value: controlledValue,
|
|
195
|
+
defaultValue,
|
|
196
|
+
min = -Infinity,
|
|
197
|
+
max = Infinity,
|
|
198
|
+
step = 1,
|
|
199
|
+
onValueChange,
|
|
200
|
+
formatValue,
|
|
201
|
+
editable = true,
|
|
202
|
+
variant = 'outline',
|
|
203
|
+
size = 'md',
|
|
204
|
+
disabled,
|
|
205
|
+
label,
|
|
206
|
+
description,
|
|
207
|
+
errorMessage,
|
|
208
|
+
isRequired,
|
|
209
|
+
haptics,
|
|
210
|
+
placeholder,
|
|
211
|
+
onFocus,
|
|
212
|
+
onBlur,
|
|
213
|
+
returnKeyType,
|
|
214
|
+
},
|
|
215
|
+
ref
|
|
216
|
+
) => {
|
|
217
|
+
const isControlled = controlledValue !== undefined;
|
|
218
|
+
const initial = normalize(
|
|
219
|
+
controlledValue ?? defaultValue ?? (isFinite(min) ? min : 0),
|
|
220
|
+
min,
|
|
221
|
+
max,
|
|
222
|
+
step
|
|
223
|
+
);
|
|
224
|
+
const [uncontrolled, setUncontrolled] = useState(initial);
|
|
225
|
+
const value = isControlled ? normalize(controlledValue!, min, max, step) : uncontrolled;
|
|
226
|
+
|
|
227
|
+
// The repeat timers read the live value through a ref, so a held button
|
|
228
|
+
// keeps counting from where it is rather than from the value it closed over.
|
|
229
|
+
const valueRef = useRef(value);
|
|
230
|
+
valueRef.current = value;
|
|
231
|
+
|
|
232
|
+
const invalid = !!errorMessage;
|
|
233
|
+
const foreground = useCSSVariable('--color-foreground');
|
|
234
|
+
const iconColor = typeof foreground === 'string' ? foreground : undefined;
|
|
235
|
+
const slots = numberInputVariants({ variant, size, invalid, disabled: !!disabled });
|
|
236
|
+
|
|
237
|
+
const format = useCallback(
|
|
238
|
+
(v: number) => (formatValue ? formatValue(v) : String(v)),
|
|
239
|
+
[formatValue]
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// `text` is what the field shows while focused; it is allowed to be
|
|
243
|
+
// half-typed. Away from focus it always mirrors the committed value.
|
|
244
|
+
const [text, setText] = useState(() => format(value));
|
|
245
|
+
const [focused, setFocused] = useState(false);
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
if (!focused) setText(format(value));
|
|
248
|
+
}, [value, focused, format]);
|
|
249
|
+
|
|
250
|
+
const commit = useCallback(
|
|
251
|
+
(next: number, tick: boolean) => {
|
|
252
|
+
const clamped = normalize(next, min, max, step);
|
|
253
|
+
if (tick && haptics) selectionTick();
|
|
254
|
+
if (!isControlled) setUncontrolled(clamped);
|
|
255
|
+
if (clamped !== value) onValueChange?.(clamped);
|
|
256
|
+
},
|
|
257
|
+
[min, max, step, haptics, isControlled, value, onValueChange]
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// --- hold-to-repeat -----------------------------------------------------
|
|
261
|
+
const delayRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
262
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
263
|
+
|
|
264
|
+
const stopRepeat = useCallback(() => {
|
|
265
|
+
if (delayRef.current) clearTimeout(delayRef.current);
|
|
266
|
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
267
|
+
delayRef.current = null;
|
|
268
|
+
intervalRef.current = null;
|
|
269
|
+
}, []);
|
|
270
|
+
|
|
271
|
+
useEffect(() => stopRepeat, [stopRepeat]);
|
|
272
|
+
|
|
273
|
+
const startRepeat = useCallback(
|
|
274
|
+
(direction: 1 | -1) => {
|
|
275
|
+
const nudge = () => commit(valueRef.current + direction * step, true);
|
|
276
|
+
nudge();
|
|
277
|
+
delayRef.current = setTimeout(() => {
|
|
278
|
+
intervalRef.current = setInterval(nudge, REPEAT_INTERVAL);
|
|
279
|
+
}, REPEAT_DELAY);
|
|
280
|
+
},
|
|
281
|
+
[commit, step]
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const atMin = value <= min;
|
|
285
|
+
const atMax = value >= max;
|
|
286
|
+
|
|
287
|
+
const handleEndEditing = useCallback(() => {
|
|
288
|
+
const parsed = parseFloat(text);
|
|
289
|
+
// An empty or unreadable field falls back to the last good value rather
|
|
290
|
+
// than snapping to a surprising 0.
|
|
291
|
+
commit(Number.isNaN(parsed) ? value : parsed, false);
|
|
292
|
+
setFocused(false);
|
|
293
|
+
setText(format(Number.isNaN(parsed) ? value : normalize(parsed, min, max, step)));
|
|
294
|
+
}, [text, value, commit, format, min, max, step]);
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<View className={slots.container({ className: containerClassName })}>
|
|
298
|
+
{label ? (
|
|
299
|
+
<Label isRequired={isRequired} isInvalid={invalid} isDisabled={!!disabled}>
|
|
300
|
+
{label}
|
|
301
|
+
</Label>
|
|
302
|
+
) : null}
|
|
303
|
+
<View className={slots.control({ className })}>
|
|
304
|
+
<AnimatedPressable
|
|
305
|
+
className={slots.button()}
|
|
306
|
+
disabled={disabled || atMin}
|
|
307
|
+
pressOpacity={0.6}
|
|
308
|
+
onPressIn={() => {
|
|
309
|
+
if (!disabled && !atMin) startRepeat(-1);
|
|
310
|
+
}}
|
|
311
|
+
onPressOut={stopRepeat}
|
|
312
|
+
accessibilityRole="button"
|
|
313
|
+
accessibilityLabel={`Decrease ${label ?? 'value'}`}
|
|
314
|
+
accessibilityState={{ disabled: disabled || atMin }}
|
|
315
|
+
style={atMin ? { opacity: 0.4 } : undefined}
|
|
316
|
+
>
|
|
317
|
+
{/* Same weight as the +, or the two ends read as different glyphs. */}
|
|
318
|
+
<MinusIcon size={ICON_SIZE[size]} color={iconColor} strokeWidth={2} />
|
|
319
|
+
</AnimatedPressable>
|
|
320
|
+
|
|
321
|
+
<TextInput
|
|
322
|
+
ref={ref}
|
|
323
|
+
editable={editable && !disabled}
|
|
324
|
+
value={text}
|
|
325
|
+
keyboardType={precisionOf(step) > 0 ? 'decimal-pad' : 'number-pad'}
|
|
326
|
+
placeholder={placeholder}
|
|
327
|
+
returnKeyType={returnKeyType}
|
|
328
|
+
selectTextOnFocus
|
|
329
|
+
onChangeText={setText}
|
|
330
|
+
onFocus={(e) => {
|
|
331
|
+
setFocused(true);
|
|
332
|
+
onFocus?.(e);
|
|
333
|
+
}}
|
|
334
|
+
onBlur={(e) => {
|
|
335
|
+
handleEndEditing();
|
|
336
|
+
onBlur?.(e);
|
|
337
|
+
}}
|
|
338
|
+
onSubmitEditing={handleEndEditing}
|
|
339
|
+
accessibilityLabel={label}
|
|
340
|
+
accessibilityState={{ disabled: !!disabled }}
|
|
341
|
+
aria-required={isRequired}
|
|
342
|
+
aria-invalid={invalid}
|
|
343
|
+
className={slots.field()}
|
|
344
|
+
style={FIELD_STYLE[size]}
|
|
345
|
+
/>
|
|
346
|
+
|
|
347
|
+
<AnimatedPressable
|
|
348
|
+
className={slots.button()}
|
|
349
|
+
disabled={disabled || atMax}
|
|
350
|
+
pressOpacity={0.6}
|
|
351
|
+
onPressIn={() => {
|
|
352
|
+
if (!disabled && !atMax) startRepeat(1);
|
|
353
|
+
}}
|
|
354
|
+
onPressOut={stopRepeat}
|
|
355
|
+
accessibilityRole="button"
|
|
356
|
+
accessibilityLabel={`Increase ${label ?? 'value'}`}
|
|
357
|
+
accessibilityState={{ disabled: disabled || atMax }}
|
|
358
|
+
style={atMax ? { opacity: 0.4 } : undefined}
|
|
359
|
+
>
|
|
360
|
+
<PlusIcon size={ICON_SIZE[size]} color={iconColor} />
|
|
361
|
+
</AnimatedPressable>
|
|
362
|
+
</View>
|
|
363
|
+
{errorMessage ? (
|
|
364
|
+
<Text className={slots.error()}>{errorMessage}</Text>
|
|
365
|
+
) : description ? (
|
|
366
|
+
<Text className={slots.description()}>{description}</Text>
|
|
367
|
+
) : null}
|
|
368
|
+
</View>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
NumberInput.displayName = 'NumberInput';
|
|
@@ -8,6 +8,7 @@ import Animated, {
|
|
|
8
8
|
} from 'react-native-reanimated';
|
|
9
9
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
10
10
|
import { getNativeUI } from '../../native';
|
|
11
|
+
import { selectionTick } from '../../utils/haptics';
|
|
11
12
|
|
|
12
13
|
const SPRING = { damping: 18, stiffness: 250, mass: 0.5 } as const;
|
|
13
14
|
|
|
@@ -48,6 +49,13 @@ export interface SwitchProps extends VariantProps<typeof switchVariants> {
|
|
|
48
49
|
native?: boolean;
|
|
49
50
|
/** Text label drawn beside the control. Native mode only. */
|
|
50
51
|
label?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Tick the haptic engine each time the switch is flipped — a toggle you feel
|
|
54
|
+
* click rather than one that merely slides. Needs the optional
|
|
55
|
+
* `expo-haptics`, and is silent without it. Ignored in `native` mode, where
|
|
56
|
+
* the platform control owns its own feedback.
|
|
57
|
+
*/
|
|
58
|
+
haptics?: boolean;
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
/**
|
|
@@ -55,7 +63,10 @@ export interface SwitchProps extends VariantProps<typeof switchVariants> {
|
|
|
55
63
|
* UI thread; toggling never re-renders beyond the value change itself.
|
|
56
64
|
*/
|
|
57
65
|
export const Switch = forwardRef<View, SwitchProps>(
|
|
58
|
-
(
|
|
66
|
+
(
|
|
67
|
+
{ className, value, onValueChange, disabled, size = 'md', native, label, haptics },
|
|
68
|
+
ref
|
|
69
|
+
) => {
|
|
59
70
|
const progress = useSharedValue(value ? 1 : 0);
|
|
60
71
|
const nativeUI = native ? getNativeUI() : null;
|
|
61
72
|
const slots = switchVariants({ size, disabled: !!disabled });
|
|
@@ -98,7 +109,10 @@ export const Switch = forwardRef<View, SwitchProps>(
|
|
|
98
109
|
accessibilityRole="switch"
|
|
99
110
|
accessibilityState={{ checked: value, disabled: !!disabled }}
|
|
100
111
|
disabled={disabled}
|
|
101
|
-
onPress={() =>
|
|
112
|
+
onPress={() => {
|
|
113
|
+
if (haptics) selectionTick();
|
|
114
|
+
onValueChange?.(!value);
|
|
115
|
+
}}
|
|
102
116
|
hitSlop={8}
|
|
103
117
|
>
|
|
104
118
|
<View className={slots.track({ className })}>
|
package/src/icons/index.tsx
CHANGED
|
@@ -80,14 +80,21 @@ export function CheckIcon({ size = 14, color, ...props }: IconProps) {
|
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
/**
|
|
84
|
+
* A single bar. Its weight is a prop because it has two jobs: it is the
|
|
85
|
+
* indeterminate mark inside a checkbox, where it has to carry the same weight
|
|
86
|
+
* as the check it stands in for, and it is the decrement half of a stepper,
|
|
87
|
+
* where it has to match the plus beside it. Those are different weights, and a
|
|
88
|
+
* bar that is heavier than the + it pairs with reads as a different glyph.
|
|
89
|
+
*/
|
|
90
|
+
export function MinusIcon({ size = 14, color, strokeWidth = 3, ...props }: IconProps) {
|
|
84
91
|
const resolved = useResolvedColor(color, '#fff');
|
|
85
92
|
return (
|
|
86
93
|
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
|
87
94
|
<Path
|
|
88
95
|
d="M5 12h14"
|
|
89
96
|
stroke={resolved}
|
|
90
|
-
strokeWidth={
|
|
97
|
+
strokeWidth={strokeWidth}
|
|
91
98
|
strokeLinecap="round"
|
|
92
99
|
strokeLinejoin="round"
|
|
93
100
|
/>
|
package/src/index.ts
CHANGED
|
@@ -229,6 +229,10 @@ export {
|
|
|
229
229
|
type InputGroupInputProps,
|
|
230
230
|
type InputGroupDecoratorProps,
|
|
231
231
|
} from './components/input-group';
|
|
232
|
+
export {
|
|
233
|
+
NumberInput,
|
|
234
|
+
type NumberInputProps,
|
|
235
|
+
} from './components/number-input';
|
|
232
236
|
export { OtpInput, type OtpInputProps } from './components/otp-input';
|
|
233
237
|
export {
|
|
234
238
|
Item,
|