@expokit/cli 0.4.0 → 0.4.1
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/bin/lib.mjs +1 -1
- package/package.json +1 -1
- package/registry/_docs/accordion.json +1 -1
- package/registry/_docs/avatar.json +6 -6
- package/registry/_docs/badge.json +3 -3
- package/registry/_docs/bottom-nav.json +6 -6
- package/registry/_docs/bottom-sheet.json +3 -3
- package/registry/_docs/bullet.json +6 -0
- package/registry/_docs/button-3d.json +2 -2
- package/registry/_docs/button.json +2 -2
- package/registry/_docs/calendar.json +5 -5
- package/registry/_docs/card.json +1 -1
- package/registry/_docs/chip-group.json +1 -1
- package/registry/_docs/confetti.json +3 -3
- package/registry/_docs/date-picker.json +4 -4
- package/registry/_docs/drawer.json +4 -4
- package/registry/_docs/flip-card.json +4 -4
- package/registry/_docs/floating-input.json +1 -1
- package/registry/_docs/grain.json +2 -2
- package/registry/_docs/isometric-bar.json +6 -0
- package/registry/_docs/liquidglass-tab.json +1 -1
- package/registry/_docs/otp-input.json +1 -1
- package/registry/_docs/payment-card.json +3 -3
- package/registry/_docs/pixel-bar.json +6 -0
- package/registry/_docs/polar-area.json +6 -0
- package/registry/_docs/progress-steps.json +4 -4
- package/registry/_docs/progress.json +2 -2
- package/registry/_docs/radio-group.json +1 -1
- package/registry/_docs/search-bar.json +3 -3
- package/registry/_docs/segmented-control.json +1 -1
- package/registry/_docs/select.json +6 -6
- package/registry/_docs/skeleton.json +1 -1
- package/registry/_docs/slider.json +6 -6
- package/registry/_docs/spinner.json +2 -2
- package/registry/_docs/stateful-button.json +1 -1
- package/registry/_docs/stepper.json +3 -3
- package/registry/_docs/tabs.json +2 -2
- package/registry/_docs/text-animation.json +4 -4
- package/registry/_docs/textarea.json +1 -1
- package/registry/_docs/tilt-card.json +3 -3
- package/registry/_docs/time-picker.json +6 -6
- package/registry/_docs/timeline-chart.json +6 -0
- package/registry/_docs/timeline.json +4 -4
- package/registry/_docs/treemap.json +6 -0
- package/registry/_docs/waterfall.json +6 -0
- package/registry/accordion/files/src/components/ui/accordion.tsx +31 -3
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +4 -1
- package/registry/alert/files/src/components/ui/alert.tsx +5 -2
- package/registry/alert/files/src/components/ui/button.tsx +32 -4
- package/registry/badge/files/src/components/ui/badge.tsx +2 -2
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +11 -5
- package/registry/button/files/src/components/ui/button.tsx +32 -4
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +33 -6
- package/registry/calendar/files/src/components/ui/calendar.tsx +62 -17
- package/registry/card/files/src/components/ui/card.tsx +25 -5
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +26 -4
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +35 -5
- package/registry/coupon/files/src/components/ui/coupon.tsx +45 -21
- package/registry/coupon/files/src/components/ui/flip-card.tsx +22 -3
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +57 -16
- package/registry/date-picker/files/src/components/ui/calendar.tsx +62 -17
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +96 -42
- package/registry/dialog/files/src/components/ui/dialog.tsx +9 -5
- package/registry/drawer/files/src/components/ui/drawer.tsx +8 -4
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +22 -3
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +75 -26
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +1 -1
- package/registry/list-item/files/src/components/ui/list-item.tsx +10 -3
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +50 -19
- package/registry/password-input/files/src/components/ui/floating-input.tsx +75 -26
- package/registry/password-input/files/src/components/ui/password-input.tsx +53 -19
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +22 -3
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +19 -15
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +57 -16
- package/registry/progress/files/src/components/ui/progress.tsx +9 -2
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +11 -4
- package/registry/radio-group/files/src/components/ui/radio-group.tsx +39 -9
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +70 -17
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +37 -9
- package/registry/select/files/src/components/ui/select.tsx +158 -83
- package/registry/slider/files/src/components/ui/slider.tsx +53 -25
- package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +12 -4
- package/registry/stepper/files/src/components/ui/stepper.tsx +76 -26
- package/registry/switch/files/src/components/ui/switch.tsx +56 -19
- package/registry/tabs/files/src/components/ui/tabs.tsx +7 -4
- package/registry/text-animation/files/src/components/ui/text-animation.tsx +7 -2
- package/registry/textarea/files/src/components/ui/textarea.tsx +104 -36
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +57 -16
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +154 -73
- package/registry/timeline/files/src/components/ui/timeline.tsx +9 -4
- package/registry/toast/files/src/components/ui/button.tsx +32 -4
- package/registry/toast/files/src/components/ui/toast.tsx +15 -6
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
AccessibilityInfo,
|
|
3
4
|
Animated,
|
|
5
|
+
Easing,
|
|
4
6
|
PanResponder,
|
|
5
7
|
Pressable,
|
|
6
8
|
StyleSheet,
|
|
7
9
|
Text,
|
|
8
10
|
View,
|
|
11
|
+
useWindowDimensions,
|
|
9
12
|
type AccessibilityActionEvent,
|
|
10
13
|
type GestureResponderEvent,
|
|
11
14
|
type StyleProp,
|
|
@@ -110,13 +113,22 @@ const SIZES: Record<SliderSize, { track: number; stepped: number; thumb: number;
|
|
|
110
113
|
md: { track: 4, stepped: 8, thumb: 26, bar: 46, icon: 20, font: 13, hit: 44 },
|
|
111
114
|
lg: { track: 6, stepped: 10, thumb: 30, bar: 58, icon: 24, font: 15, hit: 52 },
|
|
112
115
|
};
|
|
116
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
117
|
+
// content text scales all the way.
|
|
118
|
+
const FONT_CAP = 1.35;
|
|
113
119
|
const BUBBLE_H = 28;
|
|
114
120
|
const BUBBLE_GAP = 10;
|
|
115
121
|
const BUBBLE_W = 120;
|
|
116
122
|
/** Below this many points per step the thumb follows the finger and snaps on release. */
|
|
117
123
|
const SNAP_PX = 14;
|
|
124
|
+
const SMOOTH = Easing.bezier(0.32, 0.72, 0, 1);
|
|
118
125
|
|
|
119
126
|
const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));
|
|
127
|
+
/** Native-driven move to `toValue`: a snappy spring, or a short ease when Reduce Motion is on. */
|
|
128
|
+
const glide = (v: Animated.Value, toValue: number, reduce: boolean) =>
|
|
129
|
+
reduce
|
|
130
|
+
? Animated.timing(v, { toValue, duration: 150, easing: SMOOTH, useNativeDriver: true })
|
|
131
|
+
: Animated.spring(v, { toValue, useNativeDriver: true, ...motion.snappy });
|
|
120
132
|
const decimals = (n: number) => (String(n).split('.')[1] ?? '').length;
|
|
121
133
|
|
|
122
134
|
/**
|
|
@@ -163,9 +175,12 @@ export function Slider(props: SliderProps) {
|
|
|
163
175
|
|
|
164
176
|
const { colors, radius, dark, font } = useKit();
|
|
165
177
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
178
|
+
const reduce = useReduceMotion();
|
|
166
179
|
const tint = accent ?? colors.accent;
|
|
167
180
|
const onTint = accent ? readableOn(accent) : colors.accentForeground;
|
|
168
181
|
const sz = SIZES[size];
|
|
182
|
+
// Text scale the capped labels actually render at, for the label row's height.
|
|
183
|
+
const fontScale = Math.min(useWindowDimensions().fontScale, FONT_CAP);
|
|
169
184
|
|
|
170
185
|
const T = isBar ? 0 : sz.thumb; // thumb diameter
|
|
171
186
|
const inset = T / 2; // thumb centre travels [inset, w - inset]
|
|
@@ -211,29 +226,24 @@ export function Slider(props: SliderProps) {
|
|
|
211
226
|
const held = useRef(new Animated.Value(0)).current;
|
|
212
227
|
|
|
213
228
|
// Everything the (once-created) PanResponder reads, kept fresh every render.
|
|
214
|
-
const live = useRef({ vals, w, inset, step, span, min, range, isBar, T, disabled, controlled, snap, ratioOf, fmt, out, onValueChange, onSlidingStart, onSlidingComplete });
|
|
215
|
-
live.current = { vals, w, inset, step, span, min, range, isBar, T, disabled, controlled, snap, ratioOf, fmt, out, onValueChange, onSlidingStart, onSlidingComplete };
|
|
229
|
+
const live = useRef({ vals, w, inset, step, span, min, range, isBar, T, disabled, controlled, reduce, snap, ratioOf, fmt, out, onValueChange, onSlidingStart, onSlidingComplete });
|
|
230
|
+
live.current = { vals, w, inset, step, span, min, range, isBar, T, disabled, controlled, reduce, snap, ratioOf, fmt, out, onValueChange, onSlidingStart, onSlidingComplete };
|
|
216
231
|
const drag = useRef({ dragging: false, thumb: 0, startX: 0, origin: 0, relative: false, moved: false, cur: [] as number[], target: [-1, -1] });
|
|
217
232
|
|
|
218
233
|
// Follow value changes from outside (controlled updates, a11y, icon taps) when nobody is dragging.
|
|
219
234
|
const key = vals.join(',');
|
|
220
235
|
useEffect(() => {
|
|
221
236
|
if (drag.current.dragging) return;
|
|
222
|
-
vals.forEach((v, i) =>
|
|
237
|
+
vals.forEach((v, i) => glide(ratios[i], ratioOf(v), reduce).start());
|
|
223
238
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
224
239
|
}, [key, min, max]);
|
|
225
240
|
|
|
241
|
+
// With Reduce Motion these only fade (the scale / lift outputs below are flattened).
|
|
226
242
|
useEffect(() => {
|
|
227
|
-
press.forEach((p, i) =>
|
|
228
|
-
bubble.forEach((b, i) =>
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
useNativeDriver: true,
|
|
232
|
-
...motion.snappy,
|
|
233
|
-
}).start()
|
|
234
|
-
);
|
|
235
|
-
Animated.spring(held, { toValue: active !== null ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
236
|
-
}, [active, bubbleShown, showValue, press, bubble, held]);
|
|
243
|
+
press.forEach((p, i) => glide(p, active === i ? 1 : 0, reduce).start());
|
|
244
|
+
bubble.forEach((b, i) => glide(b, bubbleShown || (showValue === 'drag' && active === i) ? 1 : 0, reduce).start());
|
|
245
|
+
glide(held, active !== null ? 1 : 0, reduce).start();
|
|
246
|
+
}, [active, bubbleShown, showValue, press, bubble, held, reduce]);
|
|
237
247
|
|
|
238
248
|
const pan = useMemo(() => {
|
|
239
249
|
const usableOf = () => Math.max(1, live.current.w - 2 * live.current.inset);
|
|
@@ -249,7 +259,7 @@ export function Slider(props: SliderProps) {
|
|
|
249
259
|
const coarse = !!L.step && (L.step / L.span) * usableOf() >= SNAP_PX;
|
|
250
260
|
const target = coarse ? L.ratioOf(snapped) : L.ratioOf(v);
|
|
251
261
|
if (animate || coarse) {
|
|
252
|
-
if (target !== d.target[i])
|
|
262
|
+
if (target !== d.target[i]) glide(ratios[i], target, L.reduce).start();
|
|
253
263
|
} else {
|
|
254
264
|
ratios[i].setValue(target);
|
|
255
265
|
}
|
|
@@ -269,7 +279,7 @@ export function Slider(props: SliderProps) {
|
|
|
269
279
|
const d = drag.current;
|
|
270
280
|
if (!d.dragging) return;
|
|
271
281
|
if (seek) moveTo(d.startX, true);
|
|
272
|
-
|
|
282
|
+
glide(ratios[d.thumb], L.ratioOf(d.cur[d.thumb]), L.reduce).start();
|
|
273
283
|
d.dragging = false;
|
|
274
284
|
d.target = [-1, -1];
|
|
275
285
|
if (!L.controlled) setInner(d.cur);
|
|
@@ -350,17 +360,18 @@ export function Slider(props: SliderProps) {
|
|
|
350
360
|
};
|
|
351
361
|
}, [w, inset, usable, range, slotW, ratios]);
|
|
352
362
|
|
|
363
|
+
// Reduce Motion: no swell, no pop-up — the halo and tooltip just fade.
|
|
353
364
|
const anim = useMemo(
|
|
354
365
|
() => ({
|
|
355
|
-
thumb: press.map((p) => [{ scale: p.interpolate({ inputRange: [0, 1], outputRange: [1, 1.14] }) }]),
|
|
356
|
-
halo: press.map((p) => [{ scale: p.interpolate({ inputRange: [0, 1], outputRange: [1, 1.75] }) }]),
|
|
366
|
+
thumb: press.map((p) => [{ scale: p.interpolate({ inputRange: [0, 1], outputRange: [1, reduce ? 1 : 1.14] }) }]),
|
|
367
|
+
halo: press.map((p) => [{ scale: p.interpolate({ inputRange: [0, 1], outputRange: [1, reduce ? 1 : 1.75] }) }]),
|
|
357
368
|
bubble: bubble.map((b) => [
|
|
358
|
-
{ translateY: b.interpolate({ inputRange: [0, 1], outputRange: [6, 0] }) },
|
|
359
|
-
{ scale: b.interpolate({ inputRange: [0, 1], outputRange: [0.6, 1] }) },
|
|
369
|
+
{ translateY: b.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : 6, 0] }) },
|
|
370
|
+
{ scale: b.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.6, 1] }) },
|
|
360
371
|
]),
|
|
361
|
-
held: [{ scale: held.interpolate({ inputRange: [0, 1], outputRange: [1, 1.025] }) }],
|
|
372
|
+
held: [{ scale: held.interpolate({ inputRange: [0, 1], outputRange: [1, reduce ? 1 : 1.025] }) }],
|
|
362
373
|
}),
|
|
363
|
-
[press, bubble, held]
|
|
374
|
+
[press, bubble, held, reduce]
|
|
364
375
|
);
|
|
365
376
|
|
|
366
377
|
// ---- marks ----------------------------------------------------------------------------------
|
|
@@ -413,7 +424,7 @@ export function Slider(props: SliderProps) {
|
|
|
413
424
|
<View style={[styles.barContent, { width: w, height: trackH, paddingHorizontal: Math.round(trackH * 0.32) }]}>
|
|
414
425
|
<View>{icon !== undefined ? drawIcon(icon, iconColor) : null}</View>
|
|
415
426
|
{showValue === 'always' ? (
|
|
416
|
-
<Text numberOfLines={1} selectable={false} style={[styles.barValue, ff, { color: textColor, fontSize: sz.font + 1 }, labelStyle]}>
|
|
427
|
+
<Text numberOfLines={1} selectable={false} maxFontSizeMultiplier={FONT_CAP} style={[styles.barValue, ff, { color: textColor, fontSize: sz.font + 1 }, labelStyle]}>
|
|
417
428
|
{range ? `${fmt(vals[0])} – ${fmt(vals[1])}` : fmt(vals[0])}
|
|
418
429
|
</Text>
|
|
419
430
|
) : null}
|
|
@@ -425,6 +436,7 @@ export function Slider(props: SliderProps) {
|
|
|
425
436
|
key={key}
|
|
426
437
|
numberOfLines={1}
|
|
427
438
|
selectable={false}
|
|
439
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
428
440
|
style={[styles.markLabel, place, ff, { fontSize: sz.font - 1, color: lit ? colors.foreground : colors.mutedForeground }, labelStyle]}
|
|
429
441
|
>
|
|
430
442
|
{text}
|
|
@@ -570,7 +582,7 @@ export function Slider(props: SliderProps) {
|
|
|
570
582
|
]}
|
|
571
583
|
>
|
|
572
584
|
<View style={[styles.bubble, { backgroundColor: colors.foreground, borderRadius: radius.capsule(BUBBLE_H) }]}>
|
|
573
|
-
<Text numberOfLines={1} selectable={false} style={[styles.bubbleText, ff, { color: colors.background, fontSize: sz.font }, labelStyle]}>
|
|
585
|
+
<Text numberOfLines={1} selectable={false} maxFontSizeMultiplier={FONT_CAP} style={[styles.bubbleText, ff, { color: colors.background, fontSize: sz.font }, labelStyle]}>
|
|
574
586
|
{fmt(v)}
|
|
575
587
|
</Text>
|
|
576
588
|
</View>
|
|
@@ -583,7 +595,7 @@ export function Slider(props: SliderProps) {
|
|
|
583
595
|
</View>
|
|
584
596
|
|
|
585
597
|
{hasMarkLabels || minMax ? (
|
|
586
|
-
<View pointerEvents="none" style={[styles.labels, { height: sz.font + 8 }]}>
|
|
598
|
+
<View pointerEvents="none" style={[styles.labels, { height: Math.round(sz.font * fontScale) + 8 }]}>
|
|
587
599
|
{hasMarkLabels
|
|
588
600
|
? markList.map((m) => {
|
|
589
601
|
if (!m.label || !w) return null;
|
|
@@ -672,6 +684,22 @@ export function SliderIcon({ name, size = 20, color }: { name: SliderIconName; s
|
|
|
672
684
|
);
|
|
673
685
|
}
|
|
674
686
|
|
|
687
|
+
function useReduceMotion() {
|
|
688
|
+
const [reduce, setReduce] = useState(false);
|
|
689
|
+
useEffect(() => {
|
|
690
|
+
let alive = true;
|
|
691
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
692
|
+
.then((r) => alive && setReduce(r))
|
|
693
|
+
.catch(() => {});
|
|
694
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
695
|
+
return () => {
|
|
696
|
+
alive = false;
|
|
697
|
+
sub.remove();
|
|
698
|
+
};
|
|
699
|
+
}, []);
|
|
700
|
+
return reduce;
|
|
701
|
+
}
|
|
702
|
+
|
|
675
703
|
const styles = StyleSheet.create({
|
|
676
704
|
root: { alignSelf: 'stretch' },
|
|
677
705
|
row: { flexDirection: 'row', alignItems: 'flex-start', gap: 12 },
|
|
@@ -700,7 +728,7 @@ const styles = StyleSheet.create({
|
|
|
700
728
|
},
|
|
701
729
|
bubbleWrap: { position: 'absolute', width: BUBBLE_W, alignItems: 'center', transformOrigin: 'bottom' },
|
|
702
730
|
bubble: {
|
|
703
|
-
|
|
731
|
+
minHeight: BUBBLE_H,
|
|
704
732
|
paddingHorizontal: 10,
|
|
705
733
|
minWidth: 36,
|
|
706
734
|
alignItems: 'center',
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Pressable,
|
|
7
7
|
StyleSheet,
|
|
8
8
|
View,
|
|
9
|
+
useWindowDimensions,
|
|
9
10
|
type LayoutChangeEvent,
|
|
10
11
|
type StyleProp,
|
|
11
12
|
type TextStyle,
|
|
@@ -67,6 +68,9 @@ const SIZES: Record<StatefulButtonSize, { h: number; px: number; font: number; i
|
|
|
67
68
|
lg: { h: 54, px: 22, font: 17, icon: 20, gap: 9 },
|
|
68
69
|
};
|
|
69
70
|
const STAGGER = 22;
|
|
71
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
72
|
+
// content text scales all the way.
|
|
73
|
+
const FONT_CAP = 1.35;
|
|
70
74
|
const END = 1e6;
|
|
71
75
|
// Connected scripts (Arabic, Indic, Thai…) roll as one piece instead of letter by letter.
|
|
72
76
|
const CONNECTED = /[-ࣿऀ--ༀ-က-႟ក-יִ-﷿ﹰ-]/;
|
|
@@ -131,6 +135,7 @@ function Letters({ text, style, lh, space, anims, onLayout }: LettersProps) {
|
|
|
131
135
|
<View key={i} style={{ height: lh, overflow: 'hidden' }}>
|
|
132
136
|
<Animated.Text
|
|
133
137
|
numberOfLines={1}
|
|
138
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
134
139
|
style={[style, anims?.[i] && { opacity: anims[i].opacity, transform: [{ translateY: anims[i].translateY }] }]}
|
|
135
140
|
>
|
|
136
141
|
{ch}
|
|
@@ -252,8 +257,11 @@ export function StatefulButton({
|
|
|
252
257
|
const reduce = useReduceMotion();
|
|
253
258
|
const s = SIZES[size];
|
|
254
259
|
const fontSize = StyleSheet.flatten(textStyle)?.fontSize ?? s.font;
|
|
255
|
-
|
|
256
|
-
const
|
|
260
|
+
// Letter boxes clip to one line, so they grow with the (capped) text size; the Text's own lineHeight scales itself.
|
|
261
|
+
const textScale = Math.min(Math.max(useWindowDimensions().fontScale || 1, 1), FONT_CAP);
|
|
262
|
+
const lineHeight = Math.round(fontSize * 1.3);
|
|
263
|
+
const lh = Math.ceil(lineHeight * textScale);
|
|
264
|
+
const space = fontSize * 0.28 * textScale;
|
|
257
265
|
|
|
258
266
|
const [inner, setInner] = useState<StatefulButtonState>('idle');
|
|
259
267
|
const current = state ?? inner;
|
|
@@ -421,7 +429,7 @@ export function StatefulButton({
|
|
|
421
429
|
setWidths((prev) => (prev[text] === w ? prev : { ...prev, [text]: w }));
|
|
422
430
|
};
|
|
423
431
|
|
|
424
|
-
const textBase: StyleProp<TextStyle> = [styles.label, { fontSize: s.font, fontFamily: font.family }, textStyle, { lineHeight
|
|
432
|
+
const textBase: StyleProp<TextStyle> = [styles.label, { fontSize: s.font, fontFamily: font.family }, textStyle, { lineHeight }];
|
|
425
433
|
// Press feedback: a light wash on dark fills, a faint shade on light or transparent ones.
|
|
426
434
|
const wash = variant === 'primary' && readableOn(look[0].bg) === '#ffffff' ? alpha('#ffffff', 0.14) : alpha(colors.foreground, 0.07);
|
|
427
435
|
const corner = pill ? radius.capsule(s.h) : radius.md;
|
|
@@ -444,7 +452,7 @@ export function StatefulButton({
|
|
|
444
452
|
style={[
|
|
445
453
|
styles.surface,
|
|
446
454
|
{
|
|
447
|
-
|
|
455
|
+
minHeight: s.h,
|
|
448
456
|
minWidth: s.h,
|
|
449
457
|
paddingHorizontal: s.px,
|
|
450
458
|
borderRadius: corner,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
AccessibilityInfo,
|
|
3
4
|
Animated,
|
|
4
5
|
Pressable,
|
|
5
6
|
StyleSheet,
|
|
6
7
|
Text,
|
|
7
8
|
View,
|
|
9
|
+
useWindowDimensions,
|
|
8
10
|
type AccessibilityActionEvent,
|
|
9
11
|
type LayoutChangeEvent,
|
|
10
12
|
type StyleProp,
|
|
@@ -65,6 +67,10 @@ export interface StepperProps {
|
|
|
65
67
|
valueStyle?: StyleProp<TextStyle>;
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
71
|
+
// content text scales all the way.
|
|
72
|
+
const FONT_CAP = 1.35;
|
|
73
|
+
|
|
68
74
|
/** Every dimension in one table so the sizes read clearly different. */
|
|
69
75
|
const SIZES: Record<StepperSize | 'xs', { btn: number; font: number; icon: number; num: number }> = {
|
|
70
76
|
xs: { btn: 28, font: 15, icon: 12, num: 26 },
|
|
@@ -120,6 +126,7 @@ export function Stepper({
|
|
|
120
126
|
}: StepperProps) {
|
|
121
127
|
const { colors, radius, font } = useKit();
|
|
122
128
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
129
|
+
const reduce = useReduceMotion();
|
|
123
130
|
const variant: StepperVariant = variantProp in LEGACY ? LEGACY[variantProp as StepperLegacyVariant] : (variantProp as StepperVariant);
|
|
124
131
|
const vertical = variant !== 'cart' && (orientation ? orientation === 'vertical' : variantProp === 'vertical');
|
|
125
132
|
const tint = accent ?? colors.accent;
|
|
@@ -134,9 +141,11 @@ export function Stepper({
|
|
|
134
141
|
const [track, setTrack] = useState({ v: current, dir: 1 });
|
|
135
142
|
if (track.v !== current) setTrack({ v: current, dir: current > track.v ? 1 : -1 });
|
|
136
143
|
|
|
137
|
-
// Press past a limit: the number nudges toward the wall and springs back
|
|
144
|
+
// Press past a limit: the number nudges toward the wall and springs back (not with Reduce Motion;
|
|
145
|
+
// the dimmed button already says it's the limit).
|
|
138
146
|
const bump = useRef(new Animated.Value(0)).current;
|
|
139
147
|
const nudge = (dir: 1 | -1) => {
|
|
148
|
+
if (reduce) return;
|
|
140
149
|
Animated.sequence([
|
|
141
150
|
Animated.timing(bump, { toValue: dir * 4, duration: 60, useNativeDriver: true }),
|
|
142
151
|
Animated.spring(bump, { toValue: 0, useNativeDriver: true, ...motion.bouncy }),
|
|
@@ -238,6 +247,7 @@ export function Stepper({
|
|
|
238
247
|
limit={dir === 1 ? atMax : atMin}
|
|
239
248
|
disabled={!!disabled}
|
|
240
249
|
label={a11y ?? `${dir === 1 ? 'Increase' : 'Decrease'}${title ? ` ${title}` : ''}`}
|
|
250
|
+
reduce={reduce}
|
|
241
251
|
style={buttonStyle}
|
|
242
252
|
onPressIn={() => startRepeat(dir)}
|
|
243
253
|
onPressOut={stopRepeat}
|
|
@@ -246,9 +256,9 @@ export function Stepper({
|
|
|
246
256
|
|
|
247
257
|
const number = (
|
|
248
258
|
<View style={styles.roll}>
|
|
249
|
-
<RollingText text={text} dir={track.dir} style={numStyle} />
|
|
259
|
+
<RollingText text={text} dir={track.dir} style={numStyle} reduce={reduce} />
|
|
250
260
|
{suffixText ? (
|
|
251
|
-
<Text style={[styles.num, styles.suffix, numStyle, { fontSize: fontSize * 0.82, color: isCart ? alpha(onCart, 0.8) : colors.mutedForeground }]}>
|
|
261
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.num, styles.suffix, numStyle, { fontSize: fontSize * 0.82, color: isCart ? alpha(onCart, 0.8) : colors.mutedForeground }]}>
|
|
252
262
|
{suffixText}
|
|
253
263
|
</Text>
|
|
254
264
|
) : null}
|
|
@@ -287,6 +297,7 @@ export function Stepper({
|
|
|
287
297
|
addLabel={addLabel}
|
|
288
298
|
bump={bump}
|
|
289
299
|
disabled={!!disabled}
|
|
300
|
+
reduce={reduce}
|
|
290
301
|
dec={button(-1, removes ? ICON.trash : ICON.minus, removes ? `Remove${title ? ` ${title}` : ''}` : undefined)}
|
|
291
302
|
inc={button(1)}
|
|
292
303
|
onAdd={() => startRepeat(1)}
|
|
@@ -300,7 +311,7 @@ export function Stepper({
|
|
|
300
311
|
styles.value,
|
|
301
312
|
vertical
|
|
302
313
|
? { minHeight: s.num, minWidth: s.btn, paddingVertical: 2 }
|
|
303
|
-
: { minWidth: s.num,
|
|
314
|
+
: { minWidth: s.num, minHeight: s.btn, paddingHorizontal: variant === 'outline' ? 8 : 4 },
|
|
304
315
|
{ transform: [vertical ? { translateY: Animated.multiply(bump, -1) } : { translateX: bump }] },
|
|
305
316
|
]}
|
|
306
317
|
>
|
|
@@ -340,6 +351,7 @@ function CartControl({
|
|
|
340
351
|
addLabel,
|
|
341
352
|
bump,
|
|
342
353
|
disabled,
|
|
354
|
+
reduce,
|
|
343
355
|
dec,
|
|
344
356
|
inc,
|
|
345
357
|
onAdd,
|
|
@@ -358,6 +370,7 @@ function CartControl({
|
|
|
358
370
|
addLabel?: string;
|
|
359
371
|
bump: Animated.Value;
|
|
360
372
|
disabled: boolean;
|
|
373
|
+
reduce: boolean;
|
|
361
374
|
dec: React.ReactNode;
|
|
362
375
|
inc: React.ReactNode;
|
|
363
376
|
onAdd: () => void;
|
|
@@ -380,18 +393,20 @@ function CartControl({
|
|
|
380
393
|
|
|
381
394
|
const measured = textW > 0 && (!addLabel || addW > 0);
|
|
382
395
|
useEffect(() => {
|
|
383
|
-
// Snap until the rulers have reported; after that every change springs.
|
|
384
|
-
if (!settled.current) {
|
|
396
|
+
// Snap until the rulers have reported; after that every change springs (or snaps, with Reduce Motion).
|
|
397
|
+
if (!settled.current || reduce) {
|
|
385
398
|
width.setValue(target);
|
|
386
399
|
settled.current = measured;
|
|
387
400
|
return;
|
|
388
401
|
}
|
|
389
402
|
Animated.spring(width, { toValue: target, useNativeDriver: false, ...motion.snappy }).start();
|
|
390
|
-
}, [target, measured, width]);
|
|
403
|
+
}, [target, measured, width, reduce]);
|
|
391
404
|
|
|
392
405
|
useEffect(() => {
|
|
393
|
-
|
|
394
|
-
|
|
406
|
+
// Reduce Motion: − and the number only fade in (their slide is flattened below).
|
|
407
|
+
if (reduce) Animated.timing(open, { toValue: collapsed ? 0 : 1, duration: 150, useNativeDriver: true }).start();
|
|
408
|
+
else Animated.spring(open, { toValue: collapsed ? 0 : 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
409
|
+
}, [collapsed, open, reduce]);
|
|
395
410
|
|
|
396
411
|
const measure = (set: (n: number) => void) => (e: LayoutChangeEvent) => set(Math.ceil(e.nativeEvent.layout.width));
|
|
397
412
|
|
|
@@ -407,15 +422,15 @@ function CartControl({
|
|
|
407
422
|
{/* Off-screen rulers inside a wide box, so nothing squeezes them. */}
|
|
408
423
|
<View pointerEvents="none" style={[styles.ruler, styles.hidden]}>
|
|
409
424
|
<View onLayout={measure(setTextW)} style={styles.row}>
|
|
410
|
-
<Text style={[styles.num, numStyle]}>{text}</Text>
|
|
411
|
-
{suffixText ? <Text style={[styles.num, styles.suffix, numStyle, { fontSize: (numStyle.fontSize ?? s.font) * 0.82 }]}>{suffixText}</Text> : null}
|
|
425
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.num, numStyle]}>{text}</Text>
|
|
426
|
+
{suffixText ? <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.num, styles.suffix, numStyle, { fontSize: (numStyle.fontSize ?? s.font) * 0.82 }]}>{suffixText}</Text> : null}
|
|
412
427
|
</View>
|
|
413
428
|
</View>
|
|
414
429
|
|
|
415
430
|
<View style={[styles.cartRow, { width: openW }]} pointerEvents="box-none">
|
|
416
431
|
<Animated.View
|
|
417
432
|
pointerEvents={collapsed ? 'none' : 'auto'}
|
|
418
|
-
style={[styles.row, { opacity: open, transform: [{ translateX: open.interpolate({ inputRange: [0, 1], outputRange: [s.btn * 0.5, 0] }) }] }]}
|
|
433
|
+
style={[styles.row, { opacity: open, transform: [{ translateX: open.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : s.btn * 0.5, 0] }) }] }]}
|
|
419
434
|
>
|
|
420
435
|
{dec}
|
|
421
436
|
<Animated.View style={[styles.value, { width: valueW, height: s.btn, transform: [{ translateX: bump }] }]}>{number}</Animated.View>
|
|
@@ -441,8 +456,8 @@ function CartControl({
|
|
|
441
456
|
onPressOut={onAddEnd}
|
|
442
457
|
style={({ pressed }) => [styles.add, { height: s.btn, paddingHorizontal: Math.round(s.btn * 0.36) }, pressed && { backgroundColor: alpha(fg, 0.16) }]}
|
|
443
458
|
>
|
|
444
|
-
<Glyph d={ICON.plus} size={s.icon} color={fg} />
|
|
445
|
-
<Text style={[styles.addText, ff, { color: fg, fontSize: s.font - 2 }]}>{addLabel}</Text>
|
|
459
|
+
<Glyph d={ICON.plus} size={s.icon} color={fg} reduce={reduce} />
|
|
460
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.addText, ff, { color: fg, fontSize: s.font - 2 }]}>{addLabel}</Text>
|
|
446
461
|
</Pressable>
|
|
447
462
|
</Animated.View>
|
|
448
463
|
</View>
|
|
@@ -470,6 +485,7 @@ function StepButton({
|
|
|
470
485
|
limit,
|
|
471
486
|
disabled,
|
|
472
487
|
label,
|
|
488
|
+
reduce,
|
|
473
489
|
style,
|
|
474
490
|
onPressIn,
|
|
475
491
|
onPressOut,
|
|
@@ -485,6 +501,7 @@ function StepButton({
|
|
|
485
501
|
limit: boolean;
|
|
486
502
|
disabled: boolean;
|
|
487
503
|
label: string;
|
|
504
|
+
reduce: boolean;
|
|
488
505
|
style?: StyleProp<ViewStyle>;
|
|
489
506
|
onPressIn: () => void;
|
|
490
507
|
onPressOut: () => void;
|
|
@@ -492,7 +509,11 @@ function StepButton({
|
|
|
492
509
|
// Scale and dim share one native-driven view; nothing JS-driven touches it.
|
|
493
510
|
const scale = useRef(new Animated.Value(1)).current;
|
|
494
511
|
const dim = useRef(new Animated.Value(limit ? 0.32 : 1)).current;
|
|
495
|
-
|
|
512
|
+
// Reduce Motion: no press squeeze; the pressed background still shows the press.
|
|
513
|
+
const spring = (to: number) => {
|
|
514
|
+
if (reduce) scale.setValue(1);
|
|
515
|
+
else Animated.spring(scale, { toValue: to, useNativeDriver: true, ...motion.bouncy }).start();
|
|
516
|
+
};
|
|
496
517
|
|
|
497
518
|
useEffect(() => {
|
|
498
519
|
Animated.timing(dim, { toValue: limit ? 0.32 : 1, duration: motion.fast, useNativeDriver: true }).start();
|
|
@@ -533,22 +554,23 @@ function StepButton({
|
|
|
533
554
|
style,
|
|
534
555
|
]}
|
|
535
556
|
>
|
|
536
|
-
<Glyph d={glyph} size={icon} color={fg} />
|
|
557
|
+
<Glyph d={glyph} size={icon} color={fg} reduce={reduce} />
|
|
537
558
|
</Pressable>
|
|
538
559
|
</Animated.View>
|
|
539
560
|
);
|
|
540
561
|
}
|
|
541
562
|
|
|
542
563
|
/** Stroked icon; pops in when its shape changes (− ↔ trash). */
|
|
543
|
-
function Glyph({ d, size, color }: { d: string; size: number; color: string }) {
|
|
564
|
+
function Glyph({ d, size, color, reduce }: { d: string; size: number; color: string; reduce: boolean }) {
|
|
544
565
|
const pop = useRef(new Animated.Value(1)).current;
|
|
545
566
|
const prev = useRef(d);
|
|
546
567
|
useEffect(() => {
|
|
547
568
|
if (prev.current === d) return;
|
|
548
569
|
prev.current = d;
|
|
570
|
+
if (reduce) return;
|
|
549
571
|
pop.setValue(0.55);
|
|
550
572
|
Animated.spring(pop, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();
|
|
551
|
-
}, [d, pop]);
|
|
573
|
+
}, [d, pop, reduce]);
|
|
552
574
|
const trash = d === ICON.trash;
|
|
553
575
|
return (
|
|
554
576
|
<Animated.View style={{ transform: [{ scale: pop }] }}>
|
|
@@ -559,37 +581,48 @@ function Glyph({ d, size, color }: { d: string; size: number; color: string }) {
|
|
|
559
581
|
);
|
|
560
582
|
}
|
|
561
583
|
|
|
562
|
-
/** Text where every changed character slides out and its replacement slides in. */
|
|
563
|
-
function RollingText({ text, dir, style }: { text: string; dir: number; style: TextStyle }) {
|
|
584
|
+
/** Text where every changed character slides out and its replacement slides in (swaps at once with Reduce Motion). */
|
|
585
|
+
function RollingText({ text, dir, style, reduce }: { text: string; dir: number; style: TextStyle; reduce: boolean }) {
|
|
564
586
|
const chars = [...text];
|
|
565
587
|
return (
|
|
566
588
|
<View style={styles.roll}>
|
|
567
589
|
{chars.map((c, i) => (
|
|
568
590
|
// Keyed from the right so the ones digit stays the ones digit when the number grows.
|
|
569
|
-
<RollingChar key={chars.length - i} char={c} dir={dir} style={style} />
|
|
591
|
+
<RollingChar key={chars.length - i} char={c} dir={dir} style={style} reduce={reduce} />
|
|
570
592
|
))}
|
|
571
593
|
</View>
|
|
572
594
|
);
|
|
573
595
|
}
|
|
574
596
|
|
|
575
|
-
function RollingChar({ char, dir, style }: { char: string; dir: number; style: TextStyle }) {
|
|
597
|
+
function RollingChar({ char, dir, style, reduce }: { char: string; dir: number; style: TextStyle; reduce: boolean }) {
|
|
576
598
|
const [pair, setPair] = useState<{ cur: string; prev: string | null; dir: number }>({ cur: char, prev: null, dir });
|
|
577
599
|
if (pair.cur !== char) setPair({ cur: char, prev: pair.cur, dir });
|
|
578
600
|
const t = useRef(new Animated.Value(1)).current;
|
|
579
|
-
|
|
601
|
+
// The digit window follows the text's own (capped) Dynamic Type scale, so a scaled digit isn't cropped.
|
|
602
|
+
const { fontScale } = useWindowDimensions();
|
|
603
|
+
const h = ((style.lineHeight as number) ?? 24) * Math.min(fontScale, FONT_CAP);
|
|
580
604
|
|
|
605
|
+
// Each change rolls once (a Reduce Motion toggle alone doesn't replay it).
|
|
606
|
+
const played = useRef(pair);
|
|
581
607
|
useLayoutEffect(() => {
|
|
582
|
-
if (pair.prev === null) return;
|
|
608
|
+
if (pair.prev === null || played.current === pair) return;
|
|
609
|
+
played.current = pair;
|
|
610
|
+
if (reduce) {
|
|
611
|
+
// At 1 the old character is gone and the new one is in place: the final value, no roll.
|
|
612
|
+
t.setValue(1);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
583
615
|
t.setValue(0);
|
|
584
616
|
Animated.spring(t, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
585
|
-
}, [pair, t]);
|
|
617
|
+
}, [pair, t, reduce]);
|
|
586
618
|
|
|
587
619
|
const d = pair.dir * h * 0.8;
|
|
588
620
|
return (
|
|
589
621
|
<View style={[styles.charBox, { height: h }]}>
|
|
590
|
-
<Text style={[styles.num, style, styles.hidden]}>{pair.cur}</Text>
|
|
622
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.num, style, styles.hidden]}>{pair.cur}</Text>
|
|
591
623
|
{pair.prev !== null ? (
|
|
592
624
|
<Animated.Text
|
|
625
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
593
626
|
style={[
|
|
594
627
|
styles.num,
|
|
595
628
|
styles.layer,
|
|
@@ -607,6 +640,7 @@ function RollingChar({ char, dir, style }: { char: string; dir: number; style: T
|
|
|
607
640
|
</Animated.Text>
|
|
608
641
|
) : null}
|
|
609
642
|
<Animated.Text
|
|
643
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
610
644
|
style={[
|
|
611
645
|
styles.num,
|
|
612
646
|
styles.layer,
|
|
@@ -623,6 +657,22 @@ function RollingChar({ char, dir, style }: { char: string; dir: number; style: T
|
|
|
623
657
|
);
|
|
624
658
|
}
|
|
625
659
|
|
|
660
|
+
function useReduceMotion() {
|
|
661
|
+
const [reduce, setReduce] = useState(false);
|
|
662
|
+
useEffect(() => {
|
|
663
|
+
let alive = true;
|
|
664
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
665
|
+
.then((r) => alive && setReduce(r))
|
|
666
|
+
.catch(() => {});
|
|
667
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
668
|
+
return () => {
|
|
669
|
+
alive = false;
|
|
670
|
+
sub.remove();
|
|
671
|
+
};
|
|
672
|
+
}, []);
|
|
673
|
+
return reduce;
|
|
674
|
+
}
|
|
675
|
+
|
|
626
676
|
const styles = StyleSheet.create({
|
|
627
677
|
// Hugs its content in a column without hijacking the cross-axis alignment of a row.
|
|
628
678
|
self: { alignItems: 'flex-start', justifyContent: 'center' },
|