@expokit/cli 0.4.0 → 0.4.2
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/README.md +17 -0
- package/bin/lib.mjs +1 -1
- package/bin/liquidglass.mjs +281 -0
- package/package.json +17 -3
- 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 +2 -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/app/(tabs)/_layout.tsx +3 -94
- package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +2 -1
- package/registry/liquidglass-tab/files/src/app/_layout.tsx +2 -2
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +2 -1
- package/registry/liquidglass-tab/files/src/components/LiquidGlassTabs.tsx +110 -0
- package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +31 -6
- package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +12 -4
- package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +6 -3
- package/registry/liquidglass-tab/files/src/lib/tabs.ts +4 -2
- 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,6 +1,8 @@
|
|
|
1
1
|
import React, { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
AccessibilityInfo,
|
|
3
4
|
Animated,
|
|
5
|
+
Easing,
|
|
4
6
|
PanResponder,
|
|
5
7
|
Pressable,
|
|
6
8
|
StyleSheet,
|
|
@@ -98,6 +100,9 @@ type FlatProps = CalendarBaseProps & {
|
|
|
98
100
|
onChange?: (value: CalendarValue) => void;
|
|
99
101
|
};
|
|
100
102
|
|
|
103
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
104
|
+
// content text scales all the way.
|
|
105
|
+
const FONT_CAP = 1.35;
|
|
101
106
|
const CELL_H = 48;
|
|
102
107
|
const CIRCLE = 38;
|
|
103
108
|
const CIRCLE_TOP = 2;
|
|
@@ -205,6 +210,7 @@ export function Calendar(props: CalendarProps) {
|
|
|
205
210
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
206
211
|
const tint = accent ?? colors.accent;
|
|
207
212
|
const fmt = useFormatters(locale);
|
|
213
|
+
const reduce = useReduceMotion();
|
|
208
214
|
|
|
209
215
|
const [inner, setInner] = useState<CalendarValue>(defaultValue ?? null);
|
|
210
216
|
const current = value !== undefined ? value : inner;
|
|
@@ -252,7 +258,11 @@ export function Calendar(props: CalendarProps) {
|
|
|
252
258
|
setLeaving({ month: prev, dir });
|
|
253
259
|
slide.stopAnimation();
|
|
254
260
|
slide.setValue(0);
|
|
255
|
-
|
|
261
|
+
// Reduce Motion: a short cross-fade in place (the translate interpolations are flat).
|
|
262
|
+
const anim = reduce
|
|
263
|
+
? Animated.timing(slide, { toValue: 1, duration: 150, easing: Easing.bezier(0.32, 0.72, 0, 1), useNativeDriver: true })
|
|
264
|
+
: Animated.spring(slide, { toValue: 1, useNativeDriver: true, ...motion.gentle });
|
|
265
|
+
anim.start(({ finished }) => {
|
|
256
266
|
if (finished) setLeaving(null);
|
|
257
267
|
});
|
|
258
268
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -267,7 +277,8 @@ export function Calendar(props: CalendarProps) {
|
|
|
267
277
|
if (next !== 'days' && view === 'days') setPickYear(visible.getFullYear());
|
|
268
278
|
setView(next);
|
|
269
279
|
viewAnim.setValue(0);
|
|
270
|
-
Animated.
|
|
280
|
+
if (reduce) Animated.timing(viewAnim, { toValue: 1, duration: 150, useNativeDriver: true }).start();
|
|
281
|
+
else Animated.spring(viewAnim, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
271
282
|
};
|
|
272
283
|
|
|
273
284
|
/* selection */
|
|
@@ -332,11 +343,13 @@ export function Calendar(props: CalendarProps) {
|
|
|
332
343
|
manyKeys: many ? many.map(toDateKey).join('|') : undefined,
|
|
333
344
|
tint,
|
|
334
345
|
fullLabel: fmt.full,
|
|
346
|
+
reduce,
|
|
335
347
|
onDayPress,
|
|
336
348
|
};
|
|
337
349
|
|
|
338
350
|
const W = width || 320;
|
|
339
|
-
|
|
351
|
+
// Reduce Motion: months swap with a fade only, no sideways slide.
|
|
352
|
+
const dir = reduce ? 0 : dirRef.current;
|
|
340
353
|
const prevDisabled = view === 'days' ? monthIndex(visible) - 1 < minM : view === 'months' ? pickYear * 12 - 1 < minM : false;
|
|
341
354
|
const nextDisabled = view === 'days' ? monthIndex(visible) + 1 > maxM : view === 'months' ? (pickYear + 1) * 12 > maxM : false;
|
|
342
355
|
const decadeStart = Math.floor(pickYear / 12) * 12;
|
|
@@ -381,7 +394,7 @@ export function Calendar(props: CalendarProps) {
|
|
|
381
394
|
>
|
|
382
395
|
{title}
|
|
383
396
|
</Animated.Text>
|
|
384
|
-
{monthPicker ? <Chevron turned={view !== 'days'} color={view === 'days' ? colors.mutedForeground : tint} /> : null}
|
|
397
|
+
{monthPicker ? <Chevron turned={view !== 'days'} color={view === 'days' ? colors.mutedForeground : tint} reduce={reduce} /> : null}
|
|
385
398
|
</Pressable>
|
|
386
399
|
<View style={styles.navs}>
|
|
387
400
|
{showJump ? (
|
|
@@ -392,7 +405,9 @@ export function Calendar(props: CalendarProps) {
|
|
|
392
405
|
hitSlop={4}
|
|
393
406
|
style={({ pressed }) => [styles.todayBtn, { borderRadius: radius.capsule(32), backgroundColor: pressed ? colors.secondary : colors.muted }]}
|
|
394
407
|
>
|
|
395
|
-
<Text style={[styles.todayText, ff, { color: tint }]}>
|
|
408
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.todayText, ff, { color: tint }]}>
|
|
409
|
+
Today
|
|
410
|
+
</Text>
|
|
396
411
|
</Pressable>
|
|
397
412
|
) : null}
|
|
398
413
|
<NavButton dir={-1} disabled={prevDisabled} onPress={() => step(-1)} />
|
|
@@ -404,12 +419,12 @@ export function Calendar(props: CalendarProps) {
|
|
|
404
419
|
<Animated.View
|
|
405
420
|
style={{
|
|
406
421
|
opacity: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
407
|
-
transform: [{ scale: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [1.04, 1] }) }],
|
|
422
|
+
transform: [{ scale: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 1.04, 1] }) }],
|
|
408
423
|
}}
|
|
409
424
|
>
|
|
410
425
|
<View style={styles.weekRow} accessibilityElementsHidden importantForAccessibility="no-hide-descendants">
|
|
411
426
|
{weekdays.map((w) => (
|
|
412
|
-
<Text key={w.long} numberOfLines={1} style={[styles.weekday, ff, { color: colors.mutedForeground }]}>
|
|
427
|
+
<Text key={w.long} numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.weekday, ff, { color: colors.mutedForeground }]}>
|
|
413
428
|
{w.short}
|
|
414
429
|
</Text>
|
|
415
430
|
))}
|
|
@@ -422,7 +437,7 @@ export function Calendar(props: CalendarProps) {
|
|
|
422
437
|
styles.leaving,
|
|
423
438
|
{
|
|
424
439
|
opacity: slide.interpolate({ inputRange: [0, 0.7], outputRange: [1, 0], extrapolate: 'clamp' }),
|
|
425
|
-
transform: [{ translateX: slide.interpolate({ inputRange: [0, 1], outputRange: [0, -leaving.dir * W * 0.6] }) }],
|
|
440
|
+
transform: [{ translateX: slide.interpolate({ inputRange: [0, 1], outputRange: [0, reduce ? 0 : -leaving.dir * W * 0.6] }) }],
|
|
426
441
|
},
|
|
427
442
|
]}
|
|
428
443
|
>
|
|
@@ -446,7 +461,7 @@ export function Calendar(props: CalendarProps) {
|
|
|
446
461
|
{ minHeight: gridH ? gridH + 28 : undefined },
|
|
447
462
|
{
|
|
448
463
|
opacity: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
449
|
-
transform: [{ scale: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [0.94, 1] }) }],
|
|
464
|
+
transform: [{ scale: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.94, 1] }) }],
|
|
450
465
|
},
|
|
451
466
|
]}
|
|
452
467
|
>
|
|
@@ -488,7 +503,9 @@ export function Calendar(props: CalendarProps) {
|
|
|
488
503
|
},
|
|
489
504
|
]}
|
|
490
505
|
>
|
|
491
|
-
<Text style={[styles.pickText, ff, { color: selected ? readableOn(tint) : isNow ? tint : colors.foreground }]}>
|
|
506
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.pickText, ff, { color: selected ? readableOn(tint) : isNow ? tint : colors.foreground }]}>
|
|
507
|
+
{label}
|
|
508
|
+
</Text>
|
|
492
509
|
</View>
|
|
493
510
|
)}
|
|
494
511
|
</Pressable>
|
|
@@ -520,6 +537,7 @@ interface MonthGridProps {
|
|
|
520
537
|
manyKeys?: string;
|
|
521
538
|
tint: string;
|
|
522
539
|
fullLabel: (d: Date) => string;
|
|
540
|
+
reduce: boolean;
|
|
523
541
|
onDayPress: (d: Date) => void;
|
|
524
542
|
}
|
|
525
543
|
|
|
@@ -541,6 +559,7 @@ const MonthGrid = memo(function MonthGrid({
|
|
|
541
559
|
manyKeys,
|
|
542
560
|
tint,
|
|
543
561
|
fullLabel,
|
|
562
|
+
reduce,
|
|
544
563
|
onDayPress,
|
|
545
564
|
}: MonthGridProps) {
|
|
546
565
|
const weeks = useMemo(() => buildWeeks(month, weekStartsOn, fixedWeeks), [month, weekStartsOn, fixedWeeks]);
|
|
@@ -595,6 +614,7 @@ const MonthGrid = memo(function MonthGrid({
|
|
|
595
614
|
highlight={highlightedDates?.[key]}
|
|
596
615
|
tile={tiles && inMonth && !inBand}
|
|
597
616
|
tint={tint}
|
|
617
|
+
reduce={reduce}
|
|
598
618
|
onPress={onDayPress}
|
|
599
619
|
/>
|
|
600
620
|
);
|
|
@@ -620,6 +640,7 @@ interface DayCellProps {
|
|
|
620
640
|
highlight?: string;
|
|
621
641
|
tile: boolean;
|
|
622
642
|
tint: string;
|
|
643
|
+
reduce: boolean;
|
|
623
644
|
onPress: (d: Date) => void;
|
|
624
645
|
}
|
|
625
646
|
|
|
@@ -638,6 +659,7 @@ const DayCell = memo(function DayCell({
|
|
|
638
659
|
highlight,
|
|
639
660
|
tile,
|
|
640
661
|
tint,
|
|
662
|
+
reduce,
|
|
641
663
|
onPress,
|
|
642
664
|
}: DayCellProps) {
|
|
643
665
|
const { colors, radius, font } = useKit();
|
|
@@ -646,8 +668,10 @@ const DayCell = memo(function DayCell({
|
|
|
646
668
|
const pop = useRef(new Animated.Value(selected ? 1 : 0)).current; // native: circle scale + opacity
|
|
647
669
|
const bandIn = useRef(new Animated.Value(band === 'none' ? 0 : 1)).current; // native: band opacity
|
|
648
670
|
useEffect(() => {
|
|
649
|
-
|
|
650
|
-
|
|
671
|
+
// Reduce Motion: the selection fills in with a quick fade, no scale pop.
|
|
672
|
+
if (reduce) Animated.timing(pop, { toValue: selected ? 1 : 0, duration: 150, useNativeDriver: true }).start();
|
|
673
|
+
else Animated.spring(pop, { toValue: selected ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
|
|
674
|
+
}, [selected, pop, reduce]);
|
|
651
675
|
useEffect(() => {
|
|
652
676
|
Animated.timing(bandIn, { toValue: band === 'none' ? 0 : 1, duration: motion.normal, useNativeDriver: true }).start();
|
|
653
677
|
}, [band, bandIn]);
|
|
@@ -714,12 +738,13 @@ const DayCell = memo(function DayCell({
|
|
|
714
738
|
borderRadius: r,
|
|
715
739
|
backgroundColor: tint,
|
|
716
740
|
opacity: pop.interpolate({ inputRange: [0, 0.4], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
717
|
-
transform: [{ scale: pop.interpolate({ inputRange: [0, 1], outputRange: [0.45, 1] }) }],
|
|
741
|
+
transform: [{ scale: pop.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.45, 1] }) }],
|
|
718
742
|
},
|
|
719
743
|
]}
|
|
720
744
|
/>
|
|
721
745
|
<View pointerEvents="none" style={styles.label}>
|
|
722
746
|
<Text
|
|
747
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
723
748
|
style={[
|
|
724
749
|
styles.day,
|
|
725
750
|
ff,
|
|
@@ -772,11 +797,15 @@ function NavButton({ dir, disabled, onPress }: { dir: 1 | -1; disabled: boolean;
|
|
|
772
797
|
);
|
|
773
798
|
}
|
|
774
799
|
|
|
775
|
-
function Chevron({ turned, color }: { turned: boolean; color: string }) {
|
|
800
|
+
function Chevron({ turned, color, reduce }: { turned: boolean; color: string; reduce: boolean }) {
|
|
776
801
|
const turn = useRef(new Animated.Value(turned ? 1 : 0)).current; // native
|
|
777
802
|
useEffect(() => {
|
|
803
|
+
if (reduce) {
|
|
804
|
+
turn.setValue(turned ? 1 : 0);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
778
807
|
Animated.spring(turn, { toValue: turned ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
779
|
-
}, [turned, turn]);
|
|
808
|
+
}, [turned, turn, reduce]);
|
|
780
809
|
return (
|
|
781
810
|
<Animated.View style={{ transform: [{ rotate: turn.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '180deg'] }) }] }}>
|
|
782
811
|
<Svg width={16} height={16} viewBox="0 0 24 24" fill="none">
|
|
@@ -786,6 +815,22 @@ function Chevron({ turned, color }: { turned: boolean; color: string }) {
|
|
|
786
815
|
);
|
|
787
816
|
}
|
|
788
817
|
|
|
818
|
+
function useReduceMotion() {
|
|
819
|
+
const [reduce, setReduce] = useState(false);
|
|
820
|
+
useEffect(() => {
|
|
821
|
+
let alive = true;
|
|
822
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
823
|
+
.then((r) => alive && setReduce(r))
|
|
824
|
+
.catch(() => {});
|
|
825
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
826
|
+
return () => {
|
|
827
|
+
alive = false;
|
|
828
|
+
sub.remove();
|
|
829
|
+
};
|
|
830
|
+
}, []);
|
|
831
|
+
return reduce;
|
|
832
|
+
}
|
|
833
|
+
|
|
789
834
|
const styles = StyleSheet.create({
|
|
790
835
|
root: { alignSelf: 'stretch' },
|
|
791
836
|
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10, minHeight: 40 },
|
|
@@ -793,7 +838,7 @@ const styles = StyleSheet.create({
|
|
|
793
838
|
title: { fontSize: 17, fontWeight: '400', letterSpacing: -0.3, flexShrink: 1 },
|
|
794
839
|
navs: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
|
795
840
|
nav: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center' },
|
|
796
|
-
todayBtn: {
|
|
841
|
+
todayBtn: { minHeight: 32, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center' },
|
|
797
842
|
todayText: { fontSize: 13, fontWeight: '400' },
|
|
798
843
|
weekRow: { flexDirection: 'row', marginBottom: 6 },
|
|
799
844
|
weekday: { flex: 1, textAlign: 'center', fontSize: 13, fontWeight: '400', letterSpacing: 0.2 },
|
|
@@ -821,6 +866,6 @@ const styles = StyleSheet.create({
|
|
|
821
866
|
dot: { width: 5, height: 5, borderRadius: 2.5 },
|
|
822
867
|
pickGrid: { flexDirection: 'row', flexWrap: 'wrap', alignContent: 'center', paddingTop: 4 },
|
|
823
868
|
pickCell: { width: '33.333%', height: 56, alignItems: 'center', justifyContent: 'center' },
|
|
824
|
-
pickInner: { minWidth: 76,
|
|
869
|
+
pickInner: { minWidth: 76, minHeight: 40, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center', borderWidth: 1.5 },
|
|
825
870
|
pickText: { fontSize: 15, fontWeight: '400' },
|
|
826
871
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { createContext, useContext, useId, useRef, useState, type ComponentRef } from 'react';
|
|
1
|
+
import React, { createContext, useContext, useEffect, useId, useRef, useState, type ComponentRef } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
AccessibilityInfo,
|
|
3
4
|
Animated,
|
|
4
5
|
Image,
|
|
5
6
|
Platform,
|
|
@@ -67,6 +68,7 @@ export function Card({
|
|
|
67
68
|
const interactive = !!(onPress || onLongPress || pressable);
|
|
68
69
|
const corner = radius.scale(18);
|
|
69
70
|
const gid = 'card' + useId().replace(/[^a-zA-Z0-9]/g, '');
|
|
71
|
+
const reduce = useReduceMotion();
|
|
70
72
|
|
|
71
73
|
const scale = useRef(new Animated.Value(1)).current;
|
|
72
74
|
const glow = useRef(new Animated.Value(0)).current;
|
|
@@ -87,15 +89,17 @@ export function Card({
|
|
|
87
89
|
|
|
88
90
|
const border = (surfaces[variant].borderWidth as number | undefined) ?? 0;
|
|
89
91
|
|
|
92
|
+
// Reduce Motion: no press squish and the spotlight doesn't grow; the glow still fades in and out.
|
|
90
93
|
const pressIn = (e: GestureResponderEvent) => {
|
|
91
|
-
Animated.spring(scale, { toValue: 0.975, useNativeDriver: true, ...motion.snappy }).start();
|
|
94
|
+
if (!reduce) Animated.spring(scale, { toValue: 0.975, useNativeDriver: true, ...motion.snappy }).start();
|
|
92
95
|
if (highlight === 'none') return;
|
|
93
96
|
moveSpot(e);
|
|
94
97
|
Animated.spring(glow, { toValue: 1, useNativeDriver: true, ...motion.gentle }).start();
|
|
95
98
|
};
|
|
96
99
|
const pressOut = () => {
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
if (reduce) scale.setValue(1);
|
|
101
|
+
else Animated.spring(scale, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();
|
|
102
|
+
if (highlight !== 'none') Animated.timing(glow, { toValue: 0, duration: reduce ? 150 : 420, useNativeDriver: true }).start();
|
|
99
103
|
};
|
|
100
104
|
// Touch coordinates are relative to whichever child was hit, so work from page coordinates.
|
|
101
105
|
const clipRef = useRef<ComponentRef<typeof View>>(null);
|
|
@@ -125,7 +129,7 @@ export function Card({
|
|
|
125
129
|
transform: [
|
|
126
130
|
{ translateX: spot.x },
|
|
127
131
|
{ translateY: spot.y },
|
|
128
|
-
{ scale: glow.interpolate({ inputRange: [0, 1], outputRange: [0.4, 1] }) },
|
|
132
|
+
{ scale: glow.interpolate({ inputRange: [0, 1], outputRange: reduce ? [1, 1] : [0.4, 1] }) },
|
|
129
133
|
],
|
|
130
134
|
},
|
|
131
135
|
]}
|
|
@@ -288,6 +292,22 @@ function shadow(opacity: number, y: number, blur: number, elevation = 2): ViewSt
|
|
|
288
292
|
return { shadowColor: '#000', shadowOpacity: opacity, shadowRadius: blur / 2, shadowOffset: { width: 0, height: y }, elevation };
|
|
289
293
|
}
|
|
290
294
|
|
|
295
|
+
function useReduceMotion() {
|
|
296
|
+
const [reduce, setReduce] = useState(false);
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
let alive = true;
|
|
299
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
300
|
+
.then((r) => alive && setReduce(r))
|
|
301
|
+
.catch(() => {});
|
|
302
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
303
|
+
return () => {
|
|
304
|
+
alive = false;
|
|
305
|
+
sub.remove();
|
|
306
|
+
};
|
|
307
|
+
}, []);
|
|
308
|
+
return reduce;
|
|
309
|
+
}
|
|
310
|
+
|
|
291
311
|
const styles = StyleSheet.create({
|
|
292
312
|
clip: { overflow: 'hidden' },
|
|
293
313
|
spot: { position: 'absolute', top: 0, left: 0 },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { Animated, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
import { AccessibilityInfo, Animated, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
import Svg, { Path } from 'react-native-svg';
|
|
4
4
|
|
|
5
5
|
import { alpha, useKit } from '@/lib/kit';
|
|
@@ -39,6 +39,7 @@ export function Checkbox({
|
|
|
39
39
|
style,
|
|
40
40
|
}: CheckboxProps) {
|
|
41
41
|
const { colors, radius, font } = useKit();
|
|
42
|
+
const reduce = useReduceMotion();
|
|
42
43
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
43
44
|
const tint = accent ?? colors.accent;
|
|
44
45
|
const corner = shape === 'circle' ? size / 2 : Math.min(size / 2, radius.sm + 1);
|
|
@@ -50,12 +51,17 @@ export function Checkbox({
|
|
|
50
51
|
const burst = useRef(new Animated.Value(0)).current;
|
|
51
52
|
|
|
52
53
|
useEffect(() => {
|
|
54
|
+
// Reduce Motion: a short fill fade, no burst ring and no press squish.
|
|
55
|
+
if (reduce) {
|
|
56
|
+
Animated.timing(progress, { toValue: on ? 1 : 0, duration: 150, useNativeDriver: false }).start();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
53
59
|
Animated.spring(progress, { toValue: on ? 1 : 0, useNativeDriver: false, speed: 14, bounciness: 0 }).start();
|
|
54
60
|
if (on) {
|
|
55
61
|
burst.setValue(0);
|
|
56
62
|
Animated.timing(burst, { toValue: 1, duration: 450, useNativeDriver: true }).start();
|
|
57
63
|
}
|
|
58
|
-
}, [on, progress, burst]);
|
|
64
|
+
}, [on, progress, burst, reduce]);
|
|
59
65
|
|
|
60
66
|
const toggle = () => {
|
|
61
67
|
if (checked === undefined) setInner(!on);
|
|
@@ -72,8 +78,8 @@ export function Checkbox({
|
|
|
72
78
|
accessibilityState={{ checked: on, disabled }}
|
|
73
79
|
disabled={disabled}
|
|
74
80
|
onPress={toggle}
|
|
75
|
-
onPressIn={() => Animated.spring(press, { toValue: 0.82, useNativeDriver: true, speed: 50 }).start()}
|
|
76
|
-
onPressOut={() => Animated.spring(press, { toValue: 1, useNativeDriver: true, speed: 20, bounciness: 0 }).start()}
|
|
81
|
+
onPressIn={() => !reduce && Animated.spring(press, { toValue: 0.82, useNativeDriver: true, speed: 50, bounciness: 0 }).start()}
|
|
82
|
+
onPressOut={() => !reduce && Animated.spring(press, { toValue: 1, useNativeDriver: true, speed: 20, bounciness: 0 }).start()}
|
|
77
83
|
style={[styles.row, { opacity: disabled ? 0.5 : 1 }, style]}
|
|
78
84
|
>
|
|
79
85
|
<Animated.View style={{ transform: [{ scale: press }] }}>
|
|
@@ -139,6 +145,22 @@ export function Checkbox({
|
|
|
139
145
|
);
|
|
140
146
|
}
|
|
141
147
|
|
|
148
|
+
function useReduceMotion() {
|
|
149
|
+
const [reduce, setReduce] = useState(false);
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
let alive = true;
|
|
152
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
153
|
+
.then((r) => alive && setReduce(r))
|
|
154
|
+
.catch(() => {});
|
|
155
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
156
|
+
return () => {
|
|
157
|
+
alive = false;
|
|
158
|
+
sub.remove();
|
|
159
|
+
};
|
|
160
|
+
}, []);
|
|
161
|
+
return reduce;
|
|
162
|
+
}
|
|
163
|
+
|
|
142
164
|
const styles = StyleSheet.create({
|
|
143
165
|
row: { flexDirection: 'row', alignItems: 'flex-start', gap: 12 },
|
|
144
166
|
box: { borderWidth: 2, alignItems: 'center', justifyContent: 'center' },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
AccessibilityInfo,
|
|
3
4
|
Animated,
|
|
4
5
|
Pressable,
|
|
5
6
|
ScrollView,
|
|
@@ -56,6 +57,10 @@ export interface ChipGroupProps {
|
|
|
56
57
|
style?: StyleProp<ViewStyle>;
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
61
|
+
// content text scales all the way.
|
|
62
|
+
const FONT_CAP = 1.35;
|
|
63
|
+
|
|
59
64
|
const SIZES: Record<ChipGroupSize, { h: number; px: number; font: number; check: number }> = {
|
|
60
65
|
sm: { h: 30, px: 12, font: 13, check: 14 },
|
|
61
66
|
md: { h: 36, px: 14, font: 14.5, check: 16 },
|
|
@@ -84,6 +89,7 @@ export function ChipGroup({
|
|
|
84
89
|
style,
|
|
85
90
|
}: ChipGroupProps) {
|
|
86
91
|
const { colors } = useKit();
|
|
92
|
+
const reduce = useReduceMotion();
|
|
87
93
|
const tint = accent ?? colors.accent;
|
|
88
94
|
const items: ChipOption[] = options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o));
|
|
89
95
|
const [inner, setInner] = useState<string[]>(defaultValue);
|
|
@@ -114,6 +120,7 @@ export function ChipGroup({
|
|
|
114
120
|
showCheck={showCheck}
|
|
115
121
|
multiple={multiple}
|
|
116
122
|
disabled={disabled || o.disabled}
|
|
123
|
+
reduce={reduce}
|
|
117
124
|
/>
|
|
118
125
|
));
|
|
119
126
|
|
|
@@ -148,9 +155,10 @@ interface ChipProps {
|
|
|
148
155
|
showCheck: boolean;
|
|
149
156
|
multiple: boolean;
|
|
150
157
|
disabled?: boolean;
|
|
158
|
+
reduce: boolean;
|
|
151
159
|
}
|
|
152
160
|
|
|
153
|
-
function Chip({ option, active, onPress, variant, size, tint, showCheck, multiple, disabled }: ChipProps) {
|
|
161
|
+
function Chip({ option, active, onPress, variant, size, tint, showCheck, multiple, disabled, reduce }: ChipProps) {
|
|
154
162
|
const { colors, radius, font } = useKit();
|
|
155
163
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
156
164
|
// p: colours, check width + stroke (JS driver). press: squish (native, on the outer view).
|
|
@@ -158,11 +166,15 @@ function Chip({ option, active, onPress, variant, size, tint, showCheck, multipl
|
|
|
158
166
|
const press = useRef(new Animated.Value(1)).current;
|
|
159
167
|
|
|
160
168
|
useEffect(() => {
|
|
161
|
-
|
|
162
|
-
|
|
169
|
+
// Reduce Motion: the chip swaps state at once (no widening, no check draw).
|
|
170
|
+
if (reduce) p.setValue(active ? 1 : 0);
|
|
171
|
+
else Animated.spring(p, { toValue: active ? 1 : 0, useNativeDriver: false, ...motion.snappy }).start();
|
|
172
|
+
}, [active, p, reduce]);
|
|
163
173
|
|
|
164
|
-
const pressTo = (to: number) =>
|
|
174
|
+
const pressTo = (to: number) => {
|
|
175
|
+
if (reduce) return;
|
|
165
176
|
Animated.spring(press, { toValue: to, useNativeDriver: true, ...(to < 1 ? motion.snappy : motion.bouncy) }).start();
|
|
177
|
+
};
|
|
166
178
|
|
|
167
179
|
const look: Record<ChipGroupVariant, { bg: [string, string]; border: [string, string]; fg: [string, string] }> = {
|
|
168
180
|
solid: {
|
|
@@ -201,7 +213,7 @@ function Chip({ option, active, onPress, variant, size, tint, showCheck, multipl
|
|
|
201
213
|
style={[
|
|
202
214
|
styles.chip,
|
|
203
215
|
{
|
|
204
|
-
|
|
216
|
+
minHeight: size.h,
|
|
205
217
|
borderRadius: radius.capsule(size.h),
|
|
206
218
|
paddingHorizontal: size.px - 1,
|
|
207
219
|
backgroundColor: p.interpolate({ ...range, outputRange: l.bg }),
|
|
@@ -238,6 +250,7 @@ function Chip({ option, active, onPress, variant, size, tint, showCheck, multipl
|
|
|
238
250
|
{option.label ? (
|
|
239
251
|
<Animated.Text
|
|
240
252
|
numberOfLines={1}
|
|
253
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
241
254
|
style={[styles.label, ff, { fontSize: size.font, color: p.interpolate({ ...range, outputRange: l.fg }) }]}
|
|
242
255
|
>
|
|
243
256
|
{option.label}
|
|
@@ -245,6 +258,7 @@ function Chip({ option, active, onPress, variant, size, tint, showCheck, multipl
|
|
|
245
258
|
) : null}
|
|
246
259
|
{option.count !== undefined ? (
|
|
247
260
|
<Animated.Text
|
|
261
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
248
262
|
style={[
|
|
249
263
|
styles.count,
|
|
250
264
|
ff,
|
|
@@ -260,6 +274,22 @@ function Chip({ option, active, onPress, variant, size, tint, showCheck, multipl
|
|
|
260
274
|
);
|
|
261
275
|
}
|
|
262
276
|
|
|
277
|
+
function useReduceMotion() {
|
|
278
|
+
const [reduce, setReduce] = useState(false);
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
let alive = true;
|
|
281
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
282
|
+
.then((r) => alive && setReduce(r))
|
|
283
|
+
.catch(() => {});
|
|
284
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
285
|
+
return () => {
|
|
286
|
+
alive = false;
|
|
287
|
+
sub.remove();
|
|
288
|
+
};
|
|
289
|
+
}, []);
|
|
290
|
+
return reduce;
|
|
291
|
+
}
|
|
292
|
+
|
|
263
293
|
const styles = StyleSheet.create({
|
|
264
294
|
row: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
|
265
295
|
wrap: { flexWrap: 'wrap' },
|