@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,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { Animated, Easing, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
import { AccessibilityInfo, Animated, Easing, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
import Svg, { Defs, LinearGradient, Path, Rect, Stop } from 'react-native-svg';
|
|
4
4
|
|
|
5
5
|
import { FlipCard, useFlipProgress } from '@/components/ui/flip-card';
|
|
@@ -170,6 +170,10 @@ function contrast(a: string, b: string) {
|
|
|
170
170
|
return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
174
|
+
// content text scales all the way.
|
|
175
|
+
const FONT_CAP = 1.35;
|
|
176
|
+
|
|
173
177
|
const DAY = 86_400_000;
|
|
174
178
|
const pad2 = (n: number) => String(n).padStart(2, '0');
|
|
175
179
|
|
|
@@ -197,7 +201,7 @@ function useRemaining(at?: Date | number) {
|
|
|
197
201
|
}
|
|
198
202
|
|
|
199
203
|
/** The promo code in a dashed chip; a tap copies it and flips the icon to a check for a moment. */
|
|
200
|
-
function CodeChip({ code, color, ink, fontSize, onCopy, labels }: { code: string; color: string; ink: string; fontSize: number; onCopy?: (code: string) => void; labels: Required<CouponLabels
|
|
204
|
+
function CodeChip({ code, color, ink, fontSize, onCopy, labels, reduce }: { code: string; color: string; ink: string; fontSize: number; onCopy?: (code: string) => void; labels: Required<CouponLabels>; reduce: boolean }) {
|
|
201
205
|
const { font } = useKit();
|
|
202
206
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
203
207
|
const [copied, setCopied] = useState(false);
|
|
@@ -208,8 +212,10 @@ function CodeChip({ code, color, ink, fontSize, onCopy, labels }: { code: string
|
|
|
208
212
|
const copy = () => {
|
|
209
213
|
onCopy?.(code);
|
|
210
214
|
setCopied(true);
|
|
211
|
-
|
|
212
|
-
|
|
215
|
+
if (!reduce) {
|
|
216
|
+
pop.setValue(0.4);
|
|
217
|
+
Animated.spring(pop, { toValue: 1, ...motion.bouncy, useNativeDriver: true }).start();
|
|
218
|
+
}
|
|
213
219
|
clearTimeout(timer.current);
|
|
214
220
|
timer.current = setTimeout(() => setCopied(false), 1600);
|
|
215
221
|
};
|
|
@@ -228,7 +234,7 @@ function CodeChip({ code, color, ink, fontSize, onCopy, labels }: { code: string
|
|
|
228
234
|
<Rect x={0.75} y={0.75} width={box.w - 1.5} height={box.h - 1.5} rx={8} fill="none" stroke={alpha(color, 0.75)} strokeWidth={1.5} strokeDasharray={[5, 4]} />
|
|
229
235
|
</Svg>
|
|
230
236
|
) : null}
|
|
231
|
-
<Text numberOfLines={1} style={[{ color: ink, fontSize, fontWeight: '800', letterSpacing: 1, flexShrink: 1 }, ff]}>
|
|
237
|
+
<Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[{ color: ink, fontSize, fontWeight: '800', letterSpacing: 1, flexShrink: 1 }, ff]}>
|
|
232
238
|
{code}
|
|
233
239
|
</Text>
|
|
234
240
|
{onCopy ? (
|
|
@@ -329,6 +335,7 @@ export function Coupon({
|
|
|
329
335
|
}: CouponProps) {
|
|
330
336
|
const { colors, dark, font } = useKit();
|
|
331
337
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
338
|
+
const reduce = useReduceMotion();
|
|
332
339
|
const theme = typeof themeProp === 'object' ? themeProp : couponThemes[themeProp];
|
|
333
340
|
const fills = typeof theme.fill === 'string' ? [theme.fill] : theme.fill;
|
|
334
341
|
const accent = theme.accent ?? fills[fills.length - 1];
|
|
@@ -424,7 +431,7 @@ export function Coupon({
|
|
|
424
431
|
left !== undefined && left > 0 && !spent ? (
|
|
425
432
|
<View style={[styles.timer, { backgroundColor: timerBg }]}>
|
|
426
433
|
<Icon d={ICON.clock} size={11} color={timerInk} />
|
|
427
|
-
<Text style={[{ color: timerInk, fontSize: 10.5, fontWeight: '800', fontVariant: ['tabular-nums'] }, ff]}>
|
|
434
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: timerInk, fontSize: 10.5, fontWeight: '800', fontVariant: ['tabular-nums'] }, ff]}>
|
|
428
435
|
{labels.endsIn} {formatCountdown(left)}
|
|
429
436
|
</Text>
|
|
430
437
|
</View>
|
|
@@ -448,15 +455,15 @@ export function Coupon({
|
|
|
448
455
|
// Value panel ------------------------------------------------------------------------------------
|
|
449
456
|
const valueBlock = (
|
|
450
457
|
<View style={{ alignItems: 'center' }}>
|
|
451
|
-
{prefix ? <Text style={[{ color: valueSoft, fontSize: 10.5, fontWeight: '900', letterSpacing: 1.6 }, ff, lift]}>{prefix.toUpperCase()}</Text> : null}
|
|
458
|
+
{prefix ? <Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: valueSoft, fontSize: 10.5, fontWeight: '900', letterSpacing: 1.6 }, ff, lift]}>{prefix.toUpperCase()}</Text> : null}
|
|
452
459
|
{locked ? <Icon d={ICON.lock} size={valueSize * 0.6} color={valueInk} /> : null}
|
|
453
460
|
{/* Vertical coupons set "50% OFF" on one line when it fits, and stack a long unit ("CASHBACK"). */}
|
|
454
461
|
<View style={{ flexDirection: inline ? 'row' : 'column', alignItems: inline ? 'flex-end' : 'center', gap: inline ? 6 : 0 }}>
|
|
455
|
-
<Text numberOfLines={1} adjustsFontSizeToFit style={[{ color: valueInk, fontSize: valueSize, lineHeight: valueSize * 1.08, fontWeight: '900', letterSpacing: -1.2 }, ff, lift]}>
|
|
462
|
+
<Text numberOfLines={1} adjustsFontSizeToFit maxFontSizeMultiplier={FONT_CAP} style={[{ color: valueInk, fontSize: valueSize, lineHeight: valueSize * 1.08, fontWeight: '900', letterSpacing: -1.2 }, ff, lift]}>
|
|
456
463
|
{value}
|
|
457
464
|
</Text>
|
|
458
465
|
{unit ? (
|
|
459
|
-
<Text style={[{ color: valueInk, fontSize: horizontal ? 12.5 : 16, fontWeight: '900', letterSpacing: 2.4, marginBottom: inline ? valueSize * 0.14 : 0 }, ff, lift]}>
|
|
466
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: valueInk, fontSize: horizontal ? 12.5 : 16, fontWeight: '900', letterSpacing: 2.4, marginBottom: inline ? valueSize * 0.14 : 0 }, ff, lift]}>
|
|
460
467
|
{unit.toUpperCase()}
|
|
461
468
|
</Text>
|
|
462
469
|
) : null}
|
|
@@ -508,13 +515,13 @@ export function Coupon({
|
|
|
508
515
|
{
|
|
509
516
|
marginLeft: 'auto',
|
|
510
517
|
backgroundColor: applied ? '#16a34a' : locked || spent ? alpha(bodyInk, 0.12) : accent,
|
|
511
|
-
transform: [{ scale: pressed ? 0.95 : 1 }],
|
|
518
|
+
transform: [{ scale: pressed && !reduce ? 0.95 : 1 }],
|
|
512
519
|
paddingHorizontal: size === 'sm' ? 11 : size === 'md' ? 13 : 16,
|
|
513
520
|
},
|
|
514
521
|
]}
|
|
515
522
|
>
|
|
516
523
|
{applied || locked ? <Icon d={applied ? ICON.check : ICON.lock} size={13} color={applied ? '#ffffff' : bodySoft} /> : null}
|
|
517
|
-
<Text numberOfLines={1} style={[{ color: applied ? '#ffffff' : locked || spent ? bodySoft : readableOn(accent), fontSize: s.chip - 0.5, fontWeight: '800' }, ff]}>
|
|
524
|
+
<Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[{ color: applied ? '#ffffff' : locked || spent ? bodySoft : readableOn(accent), fontSize: s.chip - 0.5, fontWeight: '800' }, ff]}>
|
|
518
525
|
{applied ? labels.applied : locked ? labels.locked : labels.apply}
|
|
519
526
|
</Text>
|
|
520
527
|
</Pressable>
|
|
@@ -523,11 +530,11 @@ export function Coupon({
|
|
|
523
530
|
const footer =
|
|
524
531
|
expires || canFlip ? (
|
|
525
532
|
<View style={[styles.row, { justifyContent: 'flex-start', gap: 6 }]}>
|
|
526
|
-
{expires ? <Text style={[{ color: bodySoft, fontSize: 11, fontWeight: '600' }, ff]}>{expires}</Text> : null}
|
|
533
|
+
{expires ? <Text style={[{ color: bodySoft, fontSize: 11, fontWeight: '600', flexShrink: 1 }, ff]}>{expires}</Text> : null}
|
|
527
534
|
{expires && canFlip ? <Text style={[{ color: bodySoft, fontSize: 11 }, ff]}>·</Text> : null}
|
|
528
535
|
{canFlip ? (
|
|
529
536
|
<Pressable onPress={toggle} hitSlop={10} accessibilityRole="button" accessibilityLabel={labels.termsTitle}>
|
|
530
|
-
<Text style={[{ color: textAccent, fontSize: 11, fontWeight: '800', textDecorationLine: 'underline' }, ff]}>{labels.terms}</Text>
|
|
537
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: textAccent, fontSize: 11, fontWeight: '800', textDecorationLine: 'underline' }, ff]}>{labels.terms}</Text>
|
|
531
538
|
</Pressable>
|
|
532
539
|
) : null}
|
|
533
540
|
</View>
|
|
@@ -572,7 +579,7 @@ export function Coupon({
|
|
|
572
579
|
// The code never gets cut short: when it and the button don't fit side by side, the button
|
|
573
580
|
// wraps under it (and stays right-aligned).
|
|
574
581
|
<View style={[styles.row, { gap: 8, marginTop: 2, flexWrap: 'wrap' }]}>
|
|
575
|
-
{code ? <CodeChip code={code} color={textAccent} ink={bodyInk} fontSize={s.chip} onCopy={spent || locked ? undefined : onCopy} labels={labels} /> : <View />}
|
|
582
|
+
{code ? <CodeChip code={code} color={textAccent} ink={bodyInk} fontSize={s.chip} onCopy={spent || locked ? undefined : onCopy} labels={labels} reduce={reduce} /> : <View />}
|
|
576
583
|
{action}
|
|
577
584
|
</View>
|
|
578
585
|
) : null}
|
|
@@ -582,7 +589,8 @@ export function Coupon({
|
|
|
582
589
|
|
|
583
590
|
const { width: _w, height: _h, ...shape } = outline;
|
|
584
591
|
const glareClip = measured ? ticketSafeRects(outline) : undefined;
|
|
585
|
-
|
|
592
|
+
// The sweep is decorative, so Reduce Motion drops it.
|
|
593
|
+
const valueWindows = shine && !reduce && measured && !spent && !locked
|
|
586
594
|
? ticketSafeRects({ width: valueBox.width, height: valueBox.height, corners: valueCorners, edges: valueEdges, edgeSize: v.edgeSize })
|
|
587
595
|
: null;
|
|
588
596
|
|
|
@@ -630,23 +638,23 @@ export function Coupon({
|
|
|
630
638
|
<TicketShape {...shape} fill={body} style={StyleSheet.absoluteFill}>
|
|
631
639
|
<Pressable onPress={toggle} accessibilityRole="button" accessibilityHint={labels.back} style={[StyleSheet.absoluteFill, { padding: s.pad + edgePad, gap: 6, flexDirection: horizontal ? 'row' : 'column' }]}>
|
|
632
640
|
<View style={{ flex: 1, gap: 4 }}>
|
|
633
|
-
<Text style={[{ color: bodyInk, fontSize: 12, fontWeight: '800', letterSpacing: 0.3 }, ff]}>{labels.termsTitle}</Text>
|
|
641
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: bodyInk, fontSize: 12, fontWeight: '800', letterSpacing: 0.3 }, ff]}>{labels.termsTitle}</Text>
|
|
634
642
|
{terms!.slice(0, size === 'lg' ? 5 : 4).map((t, i) => (
|
|
635
643
|
<View key={i} style={{ flexDirection: 'row', gap: 6 }}>
|
|
636
|
-
<Text style={[{ color: textAccent, fontSize: 11, fontWeight: '900' }, ff]}>•</Text>
|
|
637
|
-
<Text numberOfLines={2} style={[{ color: bodySoft, fontSize: 11, lineHeight: 14.5, flex: 1 }, ff]}>
|
|
644
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: textAccent, fontSize: 11, fontWeight: '900' }, ff]}>•</Text>
|
|
645
|
+
<Text numberOfLines={2} maxFontSizeMultiplier={FONT_CAP} style={[{ color: bodySoft, fontSize: 11, lineHeight: 14.5, flex: 1 }, ff]}>
|
|
638
646
|
{t}
|
|
639
647
|
</Text>
|
|
640
648
|
</View>
|
|
641
649
|
))}
|
|
642
|
-
<Text style={[{ color: alpha(bodyInk, 0.4), fontSize: 10, fontWeight: '700', marginTop: 'auto' }, ff]}>{labels.back}</Text>
|
|
650
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: alpha(bodyInk, 0.4), fontSize: 10, fontWeight: '700', marginTop: 'auto' }, ff]}>{labels.back}</Text>
|
|
643
651
|
</View>
|
|
644
652
|
{code ? (
|
|
645
653
|
<View style={{ alignItems: 'center', justifyContent: 'center', gap: 4 }}>
|
|
646
654
|
<View style={styles.plate}>
|
|
647
655
|
<BarcodeLike code={code} width={horizontal ? 74 : 200} height={horizontal ? 54 : 46} />
|
|
648
656
|
</View>
|
|
649
|
-
<Text style={[{ color: bodyInk, fontSize: 10, fontWeight: '800', letterSpacing: 1.2 }, ff]}>{code}</Text>
|
|
657
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: bodyInk, fontSize: 10, fontWeight: '800', letterSpacing: 1.2 }, ff]}>{code}</Text>
|
|
650
658
|
</View>
|
|
651
659
|
) : null}
|
|
652
660
|
</Pressable>
|
|
@@ -668,7 +676,7 @@ export function Coupon({
|
|
|
668
676
|
return (
|
|
669
677
|
<Pressable onPress={onPress} disabled={!onPress} accessibilityRole={onPress ? 'button' : undefined} style={style}>
|
|
670
678
|
{({ pressed }) => (
|
|
671
|
-
<View style={{ transform: [{ scale: pressed ? 0.985 : 1 }] }}>
|
|
679
|
+
<View style={{ transform: [{ scale: pressed && !reduce ? 0.985 : 1 }] }}>
|
|
672
680
|
{/* The plate waits for the painted outline (no black flash on mount), and a dimmed (used /
|
|
673
681
|
expired) coupon is see-through, so it gets none. */}
|
|
674
682
|
{spent || !measured ? null : <View pointerEvents="none" style={[styles.shadow, { top: inset, bottom: inset - 4, left: inset, right: inset, borderRadius: v.r, shadowOpacity: dark ? 0.5 : 0.2 }]} />}
|
|
@@ -680,6 +688,22 @@ export function Coupon({
|
|
|
680
688
|
);
|
|
681
689
|
}
|
|
682
690
|
|
|
691
|
+
function useReduceMotion() {
|
|
692
|
+
const [reduce, setReduce] = useState(false);
|
|
693
|
+
useEffect(() => {
|
|
694
|
+
let alive = true;
|
|
695
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
696
|
+
.then((r) => alive && setReduce(r))
|
|
697
|
+
.catch(() => {});
|
|
698
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
699
|
+
return () => {
|
|
700
|
+
alive = false;
|
|
701
|
+
sub.remove();
|
|
702
|
+
};
|
|
703
|
+
}, []);
|
|
704
|
+
return reduce;
|
|
705
|
+
}
|
|
706
|
+
|
|
683
707
|
const styles = StyleSheet.create({
|
|
684
708
|
row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
|
685
709
|
center: { alignItems: 'center', justifyContent: 'center' },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import { Animated, Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
import { AccessibilityInfo, Animated, Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { motion } from '@/lib/kit';
|
|
5
5
|
|
|
@@ -48,12 +48,15 @@ const SQUEEZE = {
|
|
|
48
48
|
* The flip as an animated 0 → 1 value, springing like FlipCard's own, plus `shadowScaleX` — pass
|
|
49
49
|
* `progress` to the FlipCard and `shadowScaleX` to a TiltCard wrapped round it, so the tilt
|
|
50
50
|
* shadow narrows as the card turns edge-on instead of showing as a dark box behind it.
|
|
51
|
+
* With Reduce Motion on, the card turns over at once (no 3D turn).
|
|
51
52
|
*/
|
|
52
53
|
export function useFlipProgress(flipped: boolean) {
|
|
54
|
+
const reduce = useReduceMotion();
|
|
53
55
|
const progress = useRef(new Animated.Value(flipped ? 1 : 0)).current;
|
|
54
56
|
useEffect(() => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
if (reduce) progress.setValue(flipped ? 1 : 0);
|
|
58
|
+
else Animated.spring(progress, { toValue: flipped ? 1 : 0, ...motion.gentle, useNativeDriver: true }).start();
|
|
59
|
+
}, [flipped, progress, reduce]);
|
|
57
60
|
const shadowScaleX = useMemo(() => progress.interpolate(SQUEEZE), [progress]);
|
|
58
61
|
return { progress, shadowScaleX };
|
|
59
62
|
}
|
|
@@ -185,6 +188,22 @@ export function FlipCard({
|
|
|
185
188
|
);
|
|
186
189
|
}
|
|
187
190
|
|
|
191
|
+
function useReduceMotion() {
|
|
192
|
+
const [reduce, setReduce] = useState(false);
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
let alive = true;
|
|
195
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
196
|
+
.then((r) => alive && setReduce(r))
|
|
197
|
+
.catch(() => {});
|
|
198
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
199
|
+
return () => {
|
|
200
|
+
alive = false;
|
|
201
|
+
sub.remove();
|
|
202
|
+
};
|
|
203
|
+
}, []);
|
|
204
|
+
return reduce;
|
|
205
|
+
}
|
|
206
|
+
|
|
188
207
|
const styles = StyleSheet.create({
|
|
189
208
|
face: { overflow: 'hidden', backfaceVisibility: 'hidden' },
|
|
190
209
|
back: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { createContext, useContext, useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
AccessibilityInfo,
|
|
3
4
|
Animated,
|
|
4
5
|
Easing,
|
|
5
6
|
Image,
|
|
@@ -92,7 +93,8 @@ export interface TiltState {
|
|
|
92
93
|
height: number;
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
// `reduce` (Reduce Motion) rides along privately so layers stay flat too; useTilt still returns a TiltState.
|
|
97
|
+
const Ctx = createContext<(TiltState & { reduce: boolean }) | null>(null);
|
|
96
98
|
|
|
97
99
|
/** The surrounding TiltCard's live tilt (null outside one) — for your own layers and effects. */
|
|
98
100
|
export function useTilt() {
|
|
@@ -105,6 +107,8 @@ const PAINT = 96;
|
|
|
105
107
|
|
|
106
108
|
// Six colour cycles across the paint, so a couple of bands cross the card like real foil. Cool
|
|
107
109
|
// pinks, violets and cyans stay clean when blended over dark cards (yellow and green turn olive).
|
|
110
|
+
const SMOOTH = Easing.bezier(0.32, 0.72, 0, 1);
|
|
111
|
+
|
|
108
112
|
const HOLO = Array.from({ length: 37 }, (_, i) => ['#ff6bd6', '#b38cff', '#6fb6ff', '#5ff0ff', '#a7ffe0', '#ffc2ef'][i % 6]);
|
|
109
113
|
|
|
110
114
|
function GlarePaint({ variant }: { variant: 'soft' | 'sharp' }) {
|
|
@@ -256,6 +260,7 @@ export function TiltCard({
|
|
|
256
260
|
accessibilityLabel,
|
|
257
261
|
style,
|
|
258
262
|
}: TiltCardProps) {
|
|
263
|
+
const reduce = useReduceMotion();
|
|
259
264
|
const [size, setSize] = useState({ w: 0, h: 0 });
|
|
260
265
|
const sizeRef = useRef(size);
|
|
261
266
|
sizeRef.current = size;
|
|
@@ -270,11 +275,13 @@ export function TiltCard({
|
|
|
270
275
|
const viewRef = useRef<ComponentRef<typeof View>>(null);
|
|
271
276
|
const disabledRef = useRef(disabled);
|
|
272
277
|
disabledRef.current = disabled;
|
|
278
|
+
const reduceRef = useRef(reduce);
|
|
279
|
+
reduceRef.current = reduce;
|
|
273
280
|
|
|
274
281
|
// Idle sway: a slow figure-eight while nobody is touching.
|
|
275
282
|
const idleLoop = useRef<Animated.CompositeAnimation | null>(null);
|
|
276
283
|
const startIdle = () => {
|
|
277
|
-
if (!idle) return;
|
|
284
|
+
if (!idle || reduce) return;
|
|
278
285
|
const ease = Easing.inOut(Easing.sin);
|
|
279
286
|
const leg = (v: Animated.Value, to: number, ms: number) =>
|
|
280
287
|
Animated.timing(v, {
|
|
@@ -292,10 +299,15 @@ export function TiltCard({
|
|
|
292
299
|
idleLoop.current.start();
|
|
293
300
|
};
|
|
294
301
|
useEffect(() => {
|
|
302
|
+
// Reduce Motion: no sway; settle where it stopped back at rest.
|
|
303
|
+
if (reduce && !touching.current) {
|
|
304
|
+
tx.setValue(0);
|
|
305
|
+
ty.setValue(0);
|
|
306
|
+
}
|
|
295
307
|
startIdle();
|
|
296
308
|
return () => idleLoop.current?.stop();
|
|
297
309
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
298
|
-
}, [idle]);
|
|
310
|
+
}, [idle, reduce]);
|
|
299
311
|
|
|
300
312
|
const aim = (pageX: number, pageY: number) => {
|
|
301
313
|
const { w, h } = sizeRef.current;
|
|
@@ -307,6 +319,12 @@ export function TiltCard({
|
|
|
307
319
|
const release = (tapped: boolean) => {
|
|
308
320
|
touching.current = false;
|
|
309
321
|
if (tapped && !moved.current) onPressRef.current?.();
|
|
322
|
+
if (reduce) {
|
|
323
|
+
// The card never leaves flat, so only the light has to settle: a short ease, no spring.
|
|
324
|
+
const settle = (v: Animated.Value) => Animated.timing(v, { toValue: 0, duration: 150, easing: SMOOTH, useNativeDriver: true });
|
|
325
|
+
Animated.parallel([settle(tx), settle(ty), settle(press)]).start();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
310
328
|
Animated.parallel([
|
|
311
329
|
Animated.spring(tx, {
|
|
312
330
|
toValue: 0,
|
|
@@ -355,11 +373,15 @@ export function TiltCard({
|
|
|
355
373
|
if (typeof px === 'number' && typeof py === 'number') origin.current = { x: px, y: py };
|
|
356
374
|
});
|
|
357
375
|
aim(pageX, pageY);
|
|
358
|
-
|
|
359
|
-
toValue: 1,
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
376
|
+
if (reduceRef.current) {
|
|
377
|
+
Animated.timing(press, { toValue: 1, duration: 150, easing: SMOOTH, useNativeDriver: true }).start();
|
|
378
|
+
} else {
|
|
379
|
+
Animated.spring(press, {
|
|
380
|
+
toValue: 1,
|
|
381
|
+
...motion.snappy,
|
|
382
|
+
useNativeDriver: true,
|
|
383
|
+
}).start();
|
|
384
|
+
}
|
|
363
385
|
},
|
|
364
386
|
onPanResponderMove: (e, g) => {
|
|
365
387
|
if (Math.abs(g.dx) + Math.abs(g.dy) > 6) moved.current = true;
|
|
@@ -380,21 +402,24 @@ export function TiltCard({
|
|
|
380
402
|
if (width !== size.w || height !== size.h) setSize({ w: width, h: height });
|
|
381
403
|
};
|
|
382
404
|
|
|
405
|
+
// Reduce Motion keeps the card flat and still: no tilt, lift or parallax. The reflection (light,
|
|
406
|
+
// not movement of the content) still follows the finger.
|
|
407
|
+
const tilt = reduce ? 0 : maxTilt;
|
|
383
408
|
const rotateY = tx.interpolate({
|
|
384
409
|
inputRange: [-1, 1],
|
|
385
|
-
outputRange: [`${-
|
|
410
|
+
outputRange: [`${-tilt}deg`, `${tilt}deg`],
|
|
386
411
|
});
|
|
387
412
|
const rotateX = ty.interpolate({
|
|
388
413
|
inputRange: [-1, 1],
|
|
389
|
-
outputRange: [`${
|
|
414
|
+
outputRange: [`${tilt}deg`, `${-tilt}deg`],
|
|
390
415
|
});
|
|
391
416
|
const scale = press.interpolate({
|
|
392
417
|
inputRange: [0, 1],
|
|
393
|
-
outputRange: [1, pressScale],
|
|
418
|
+
outputRange: [1, reduce ? 1 : pressScale],
|
|
394
419
|
});
|
|
395
420
|
|
|
396
|
-
const ctx = useMemo(() => ({ tx, ty, press, width: size.w, height: size.h }), [tx, ty, press, size.w, size.h]);
|
|
397
|
-
const drift = Math.min(10, size.w * 0.03);
|
|
421
|
+
const ctx = useMemo(() => ({ tx, ty, press, width: size.w, height: size.h, reduce }), [tx, ty, press, size.w, size.h, reduce]);
|
|
422
|
+
const drift = reduce ? 0 : Math.min(10, size.w * 0.03);
|
|
398
423
|
// The plate sits well inside the outline — far enough that it never shows through a notch or a
|
|
399
424
|
// see-through corner, even at full tilt; the shadow offset does the downward shift.
|
|
400
425
|
const body = shadowRect ?? { x: 0, y: 0, width: size.w, height: size.h };
|
|
@@ -433,7 +458,7 @@ export function TiltCard({
|
|
|
433
458
|
{
|
|
434
459
|
scale: press.interpolate({
|
|
435
460
|
inputRange: [0, 1],
|
|
436
|
-
outputRange: [1, 1.035],
|
|
461
|
+
outputRange: [1, reduce ? 1 : 1.035],
|
|
437
462
|
}),
|
|
438
463
|
},
|
|
439
464
|
{ scaleX: shadowScaleX ?? 1 },
|
|
@@ -516,7 +541,7 @@ export function TiltCard({
|
|
|
516
541
|
export function TiltLayer({ children, depth = 0.5, style }: TiltLayerProps) {
|
|
517
542
|
const ctx = useContext(Ctx);
|
|
518
543
|
if (!ctx) throw new Error('TiltLayer must be inside TiltCard');
|
|
519
|
-
const d = depth * 14;
|
|
544
|
+
const d = ctx.reduce ? 0 : depth * 14;
|
|
520
545
|
return (
|
|
521
546
|
<Animated.View
|
|
522
547
|
pointerEvents="box-none"
|
|
@@ -539,7 +564,7 @@ export function TiltLayer({ children, depth = 0.5, style }: TiltLayerProps) {
|
|
|
539
564
|
{
|
|
540
565
|
scale: ctx.press.interpolate({
|
|
541
566
|
inputRange: [0, 1],
|
|
542
|
-
outputRange: [1, 1 + Math.max(0, depth) * 0.035],
|
|
567
|
+
outputRange: [1, ctx.reduce ? 1 : 1 + Math.max(0, depth) * 0.035],
|
|
543
568
|
}),
|
|
544
569
|
},
|
|
545
570
|
],
|
|
@@ -616,6 +641,22 @@ export function TiltFoil({ children, radius = 8, base = '#d7dbe2', style }: Tilt
|
|
|
616
641
|
);
|
|
617
642
|
}
|
|
618
643
|
|
|
644
|
+
function useReduceMotion() {
|
|
645
|
+
const [reduce, setReduce] = useState(false);
|
|
646
|
+
useEffect(() => {
|
|
647
|
+
let alive = true;
|
|
648
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
649
|
+
.then((r) => alive && setReduce(r))
|
|
650
|
+
.catch(() => {});
|
|
651
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
652
|
+
return () => {
|
|
653
|
+
alive = false;
|
|
654
|
+
sub.remove();
|
|
655
|
+
};
|
|
656
|
+
}, []);
|
|
657
|
+
return reduce;
|
|
658
|
+
}
|
|
659
|
+
|
|
619
660
|
const styles = StyleSheet.create({
|
|
620
661
|
plate: {
|
|
621
662
|
position: 'absolute',
|
|
@@ -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
|
});
|