@expokit/cli 0.3.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/README.md +8 -4
- package/bin/expo-kit.js +137 -24
- package/bin/lib.mjs +110 -1
- package/bin/mcp.mjs +40 -18
- package/package.json +2 -2
- package/registry/_docs/accordion.json +1 -1
- package/registry/_docs/assistant-app.json +47 -0
- package/registry/_docs/avatar.json +6 -6
- package/registry/_docs/badge.json +4 -4
- 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 +3 -3
- package/registry/_docs/button.json +2 -2
- package/registry/_docs/calendar.json +5 -5
- package/registry/_docs/card.json +1 -1
- package/registry/_docs/checkout.json +7 -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/donut-shop.json +88 -0
- package/registry/_docs/drawer.json +4 -4
- package/registry/_docs/fintech-app.json +49 -0
- package/registry/_docs/flip-card.json +4 -4
- package/registry/_docs/floating-input.json +5 -5
- package/registry/_docs/food-app.json +47 -0
- package/registry/_docs/glass.json +1 -1
- package/registry/_docs/grain.json +80 -0
- package/registry/_docs/isometric-bar.json +6 -0
- package/registry/_docs/liquidglass-tab.json +1 -1
- package/registry/_docs/morphing-button-group.json +96 -0
- package/registry/_docs/orb-reveal.json +132 -0
- package/registry/_docs/otp-input.json +4 -4
- package/registry/_docs/password-input.json +3 -3
- 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/qr-code-button.json +122 -0
- package/registry/_docs/radio-group.json +1 -1
- package/registry/_docs/search-bar.json +4 -4
- package/registry/_docs/segmented-control.json +1 -1
- package/registry/_docs/select.json +7 -7
- package/registry/_docs/settings.json +6 -0
- package/registry/_docs/skeleton.json +3 -3
- package/registry/_docs/slider.json +6 -6
- package/registry/_docs/spinner.json +3 -3
- package/registry/_docs/stateful-button.json +1 -1
- package/registry/_docs/stepper.json +7 -7
- package/registry/_docs/tabs.json +2 -2
- package/registry/_docs/text-animation.json +4 -4
- package/registry/_docs/textarea.json +3 -3
- package/registry/_docs/ticket-shape.json +1 -1
- package/registry/_docs/tilt-card.json +4 -4
- package/registry/_docs/time-picker.json +6 -6
- package/registry/_docs/timeline-chart.json +6 -0
- package/registry/_docs/timeline.json +6 -6
- package/registry/_docs/toast.json +8 -8
- package/registry/_docs/treemap.json +6 -0
- package/registry/_docs/waterfall.json +6 -0
- package/registry/accordion/files/src/components/ui/accordion.tsx +36 -7
- package/registry/accordion/files/src/lib/kit.tsx +8 -5
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +12 -8
- package/registry/action-sheet/files/src/lib/kit.tsx +8 -5
- package/registry/alert/files/src/components/ui/alert.tsx +11 -5
- package/registry/alert/files/src/components/ui/button.tsx +35 -6
- package/registry/alert/files/src/lib/kit.tsx +8 -5
- package/registry/assistant-app.json +49 -0
- package/registry/avatar/files/src/components/ui/avatar.tsx +7 -4
- package/registry/avatar/files/src/lib/kit.tsx +8 -5
- package/registry/badge/files/src/components/ui/badge.tsx +23 -9
- package/registry/badge/files/src/lib/kit.tsx +8 -5
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +21 -8
- package/registry/bottom-nav/files/src/lib/kit.tsx +8 -5
- package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +7 -6
- package/registry/bottom-sheet/files/src/lib/kit.tsx +8 -5
- package/registry/button/files/src/components/ui/button.tsx +35 -6
- package/registry/button/files/src/lib/kit.tsx +8 -5
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +35 -7
- package/registry/button-3d/files/src/lib/kit.tsx +8 -5
- package/registry/calendar/files/src/components/ui/calendar.tsx +68 -19
- package/registry/calendar/files/src/lib/kit.tsx +8 -5
- package/registry/card/files/src/components/ui/card.tsx +32 -10
- package/registry/card/files/src/lib/kit.tsx +8 -5
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +31 -8
- package/registry/checkbox/files/src/lib/kit.tsx +8 -5
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +39 -7
- package/registry/chip-group/files/src/lib/kit.tsx +8 -5
- package/registry/confetti/files/src/lib/kit.tsx +8 -5
- package/registry/coupon/files/src/components/ui/coupon.tsx +55 -28
- package/registry/coupon/files/src/components/ui/flip-card.tsx +23 -4
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +59 -18
- package/registry/coupon/files/src/lib/kit.tsx +8 -5
- package/registry/date-picker/files/src/components/ui/calendar.tsx +68 -19
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +98 -44
- package/registry/date-picker/files/src/lib/kit.tsx +8 -5
- package/registry/dialog/files/src/components/ui/dialog.tsx +19 -9
- package/registry/dialog/files/src/lib/kit.tsx +8 -5
- package/registry/donut-shop.json +18 -0
- package/registry/drawer/files/src/components/ui/drawer.tsx +31 -21
- package/registry/drawer/files/src/lib/kit.tsx +8 -5
- package/registry/fintech-app.json +57 -0
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +23 -4
- package/registry/flip-card/files/src/lib/kit.tsx +8 -5
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +91 -32
- package/registry/floating-input/files/src/lib/kit.tsx +8 -5
- package/registry/food-app.json +55 -0
- package/registry/grain/files/src/components/ui/grain.tsx +113 -0
- package/registry/{tag-input → grain}/files/src/lib/kit.tsx +8 -5
- package/registry/grain.json +17 -0
- package/registry/kit/files/src/lib/kit.tsx +8 -5
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +2 -2
- package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +1 -1
- package/registry/list-item/files/src/components/ui/list-item.tsx +28 -17
- package/registry/list-item/files/src/lib/kit.tsx +8 -5
- package/registry/morphing-button-group.json +18 -0
- package/registry/orb-reveal.json +20 -0
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +57 -24
- package/registry/otp-input/files/src/lib/kit.tsx +8 -5
- package/registry/password-input/files/src/components/ui/floating-input.tsx +91 -32
- package/registry/password-input/files/src/components/ui/password-input.tsx +55 -21
- package/registry/password-input/files/src/lib/kit.tsx +8 -5
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +23 -4
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +35 -18
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +59 -18
- package/registry/payment-card/files/src/lib/kit.tsx +8 -5
- package/registry/progress/files/src/components/ui/progress.tsx +16 -6
- package/registry/progress/files/src/lib/kit.tsx +8 -5
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +22 -11
- package/registry/progress-steps/files/src/lib/kit.tsx +8 -5
- package/registry/qr-code-button.json +18 -0
- package/registry/radio-group/files/src/components/ui/radio-group.tsx +48 -17
- package/registry/radio-group/files/src/lib/kit.tsx +8 -5
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +81 -26
- package/registry/search-bar/files/src/lib/kit.tsx +8 -5
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +40 -10
- package/registry/segmented-control/files/src/lib/kit.tsx +8 -5
- package/registry/select/files/src/components/ui/select.tsx +161 -86
- package/registry/select/files/src/lib/kit.tsx +8 -5
- package/registry/skeleton/files/src/components/ui/skeleton.tsx +4 -4
- package/registry/skeleton/files/src/lib/kit.tsx +8 -5
- package/registry/slider/files/src/components/ui/slider.tsx +56 -27
- package/registry/slider/files/src/lib/kit.tsx +8 -5
- package/registry/spinner/files/src/components/ui/spinner.tsx +17 -12
- package/registry/spinner/files/src/lib/kit.tsx +8 -5
- package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +12 -4
- package/registry/stateful-button/files/src/lib/kit.tsx +8 -5
- package/registry/stepper/files/src/components/ui/stepper.tsx +83 -29
- package/registry/stepper/files/src/lib/kit.tsx +8 -5
- package/registry/switch/files/src/components/ui/switch.tsx +60 -22
- package/registry/switch/files/src/lib/kit.tsx +8 -5
- package/registry/tabs/files/src/components/ui/tabs.tsx +15 -10
- package/registry/tabs/files/src/lib/kit.tsx +8 -5
- package/registry/text-animation/files/src/components/ui/text-animation.tsx +11 -6
- package/registry/text-animation/files/src/lib/kit.tsx +8 -5
- package/registry/textarea/files/src/components/ui/textarea.tsx +115 -45
- package/registry/textarea/files/src/lib/kit.tsx +8 -5
- package/registry/ticket-shape/files/src/lib/kit.tsx +8 -5
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +59 -18
- package/registry/tilt-card/files/src/lib/kit.tsx +8 -5
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +156 -75
- package/registry/time-picker/files/src/lib/kit.tsx +8 -5
- package/registry/timeline/files/src/components/ui/avatar.tsx +7 -4
- package/registry/timeline/files/src/components/ui/timeline.tsx +19 -12
- package/registry/timeline/files/src/lib/kit.tsx +8 -5
- package/registry/toast/files/src/components/ui/button.tsx +35 -6
- package/registry/toast/files/src/components/ui/toast.tsx +25 -14
- package/registry/toast/files/src/lib/kit.tsx +8 -5
- package/schema/components.schema.json +1 -1
- package/registry/_docs/color-swatch.json +0 -82
- package/registry/_docs/kpi-card.json +0 -14
- package/registry/_docs/like-button.json +0 -92
- package/registry/_docs/morph-button.json +0 -128
- package/registry/_docs/pagination-dots.json +0 -108
- package/registry/_docs/shimmer-button.json +0 -98
- package/registry/_docs/tag-input.json +0 -148
- package/registry/color-swatch.json +0 -17
- package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +0 -45
- package/registry/kpi-card.json +0 -14
- package/registry/like-button.json +0 -18
- package/registry/morph-button.json +0 -18
- package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +0 -262
- package/registry/pagination-dots/files/src/lib/kit.tsx +0 -219
- package/registry/pagination-dots.json +0 -15
- package/registry/shimmer-button.json +0 -17
- package/registry/tag-input/files/src/components/ui/tag-input.tsx +0 -720
- package/registry/tag-input.json +0 -17
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
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;
|
|
@@ -267,14 +272,16 @@ export function TiltCard({
|
|
|
267
272
|
const touching = useRef(false);
|
|
268
273
|
const origin = useRef({ x: 0, y: 0 });
|
|
269
274
|
const moved = useRef(false);
|
|
270
|
-
const viewRef = useRef<View
|
|
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',
|
|
@@ -181,14 +181,17 @@ export function useKit(): Theme {
|
|
|
181
181
|
return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* Shared motion presets so the whole kit moves the same way. Every spring is critically damped
|
|
186
|
+
* (damping ≥ 2·√(stiffness·mass)): it settles smoothly with no overshoot or wobble.
|
|
187
|
+
*/
|
|
185
188
|
export const motion = {
|
|
186
189
|
/** Toggles, selection indicators. */
|
|
187
|
-
snappy: { damping:
|
|
188
|
-
/** Press feedback, pops. */
|
|
189
|
-
bouncy: { damping:
|
|
190
|
+
snappy: { damping: 30, stiffness: 260, mass: 0.8 },
|
|
191
|
+
/** Press feedback, pops. (Kept under its old name; it no longer bounces.) */
|
|
192
|
+
bouncy: { damping: 26, stiffness: 220, mass: 0.7 },
|
|
190
193
|
/** Sheets, layout changes. */
|
|
191
|
-
gentle: { damping:
|
|
194
|
+
gentle: { damping: 27, stiffness: 160, mass: 1 },
|
|
192
195
|
fast: 160,
|
|
193
196
|
normal: 240,
|
|
194
197
|
};
|
|
@@ -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;
|
|
@@ -201,9 +206,11 @@ export function Calendar(props: CalendarProps) {
|
|
|
201
206
|
accent,
|
|
202
207
|
style,
|
|
203
208
|
} = props as unknown as FlatProps;
|
|
204
|
-
const { colors, radius } = useKit();
|
|
209
|
+
const { colors, radius, font } = useKit();
|
|
210
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
205
211
|
const tint = accent ?? colors.accent;
|
|
206
212
|
const fmt = useFormatters(locale);
|
|
213
|
+
const reduce = useReduceMotion();
|
|
207
214
|
|
|
208
215
|
const [inner, setInner] = useState<CalendarValue>(defaultValue ?? null);
|
|
209
216
|
const current = value !== undefined ? value : inner;
|
|
@@ -251,7 +258,11 @@ export function Calendar(props: CalendarProps) {
|
|
|
251
258
|
setLeaving({ month: prev, dir });
|
|
252
259
|
slide.stopAnimation();
|
|
253
260
|
slide.setValue(0);
|
|
254
|
-
|
|
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 }) => {
|
|
255
266
|
if (finished) setLeaving(null);
|
|
256
267
|
});
|
|
257
268
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -266,7 +277,8 @@ export function Calendar(props: CalendarProps) {
|
|
|
266
277
|
if (next !== 'days' && view === 'days') setPickYear(visible.getFullYear());
|
|
267
278
|
setView(next);
|
|
268
279
|
viewAnim.setValue(0);
|
|
269
|
-
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();
|
|
270
282
|
};
|
|
271
283
|
|
|
272
284
|
/* selection */
|
|
@@ -331,11 +343,13 @@ export function Calendar(props: CalendarProps) {
|
|
|
331
343
|
manyKeys: many ? many.map(toDateKey).join('|') : undefined,
|
|
332
344
|
tint,
|
|
333
345
|
fullLabel: fmt.full,
|
|
346
|
+
reduce,
|
|
334
347
|
onDayPress,
|
|
335
348
|
};
|
|
336
349
|
|
|
337
350
|
const W = width || 320;
|
|
338
|
-
|
|
351
|
+
// Reduce Motion: months swap with a fade only, no sideways slide.
|
|
352
|
+
const dir = reduce ? 0 : dirRef.current;
|
|
339
353
|
const prevDisabled = view === 'days' ? monthIndex(visible) - 1 < minM : view === 'months' ? pickYear * 12 - 1 < minM : false;
|
|
340
354
|
const nextDisabled = view === 'days' ? monthIndex(visible) + 1 > maxM : view === 'months' ? (pickYear + 1) * 12 > maxM : false;
|
|
341
355
|
const decadeStart = Math.floor(pickYear / 12) * 12;
|
|
@@ -368,6 +382,7 @@ export function Calendar(props: CalendarProps) {
|
|
|
368
382
|
numberOfLines={1}
|
|
369
383
|
style={[
|
|
370
384
|
styles.title,
|
|
385
|
+
ff,
|
|
371
386
|
{ color: view === 'days' ? colors.foreground : tint },
|
|
372
387
|
view === 'days'
|
|
373
388
|
? {
|
|
@@ -379,7 +394,7 @@ export function Calendar(props: CalendarProps) {
|
|
|
379
394
|
>
|
|
380
395
|
{title}
|
|
381
396
|
</Animated.Text>
|
|
382
|
-
{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}
|
|
383
398
|
</Pressable>
|
|
384
399
|
<View style={styles.navs}>
|
|
385
400
|
{showJump ? (
|
|
@@ -390,7 +405,9 @@ export function Calendar(props: CalendarProps) {
|
|
|
390
405
|
hitSlop={4}
|
|
391
406
|
style={({ pressed }) => [styles.todayBtn, { borderRadius: radius.capsule(32), backgroundColor: pressed ? colors.secondary : colors.muted }]}
|
|
392
407
|
>
|
|
393
|
-
<Text style={[styles.todayText, { color: tint }]}>
|
|
408
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.todayText, ff, { color: tint }]}>
|
|
409
|
+
Today
|
|
410
|
+
</Text>
|
|
394
411
|
</Pressable>
|
|
395
412
|
) : null}
|
|
396
413
|
<NavButton dir={-1} disabled={prevDisabled} onPress={() => step(-1)} />
|
|
@@ -402,12 +419,12 @@ export function Calendar(props: CalendarProps) {
|
|
|
402
419
|
<Animated.View
|
|
403
420
|
style={{
|
|
404
421
|
opacity: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
405
|
-
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] }) }],
|
|
406
423
|
}}
|
|
407
424
|
>
|
|
408
425
|
<View style={styles.weekRow} accessibilityElementsHidden importantForAccessibility="no-hide-descendants">
|
|
409
426
|
{weekdays.map((w) => (
|
|
410
|
-
<Text key={w.long} numberOfLines={1} style={[styles.weekday, { color: colors.mutedForeground }]}>
|
|
427
|
+
<Text key={w.long} numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.weekday, ff, { color: colors.mutedForeground }]}>
|
|
411
428
|
{w.short}
|
|
412
429
|
</Text>
|
|
413
430
|
))}
|
|
@@ -420,7 +437,7 @@ export function Calendar(props: CalendarProps) {
|
|
|
420
437
|
styles.leaving,
|
|
421
438
|
{
|
|
422
439
|
opacity: slide.interpolate({ inputRange: [0, 0.7], outputRange: [1, 0], extrapolate: 'clamp' }),
|
|
423
|
-
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] }) }],
|
|
424
441
|
},
|
|
425
442
|
]}
|
|
426
443
|
>
|
|
@@ -444,7 +461,7 @@ export function Calendar(props: CalendarProps) {
|
|
|
444
461
|
{ minHeight: gridH ? gridH + 28 : undefined },
|
|
445
462
|
{
|
|
446
463
|
opacity: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
447
|
-
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] }) }],
|
|
448
465
|
},
|
|
449
466
|
]}
|
|
450
467
|
>
|
|
@@ -486,7 +503,9 @@ export function Calendar(props: CalendarProps) {
|
|
|
486
503
|
},
|
|
487
504
|
]}
|
|
488
505
|
>
|
|
489
|
-
<Text style={[styles.pickText, { 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>
|
|
490
509
|
</View>
|
|
491
510
|
)}
|
|
492
511
|
</Pressable>
|
|
@@ -518,6 +537,7 @@ interface MonthGridProps {
|
|
|
518
537
|
manyKeys?: string;
|
|
519
538
|
tint: string;
|
|
520
539
|
fullLabel: (d: Date) => string;
|
|
540
|
+
reduce: boolean;
|
|
521
541
|
onDayPress: (d: Date) => void;
|
|
522
542
|
}
|
|
523
543
|
|
|
@@ -539,6 +559,7 @@ const MonthGrid = memo(function MonthGrid({
|
|
|
539
559
|
manyKeys,
|
|
540
560
|
tint,
|
|
541
561
|
fullLabel,
|
|
562
|
+
reduce,
|
|
542
563
|
onDayPress,
|
|
543
564
|
}: MonthGridProps) {
|
|
544
565
|
const weeks = useMemo(() => buildWeeks(month, weekStartsOn, fixedWeeks), [month, weekStartsOn, fixedWeeks]);
|
|
@@ -593,6 +614,7 @@ const MonthGrid = memo(function MonthGrid({
|
|
|
593
614
|
highlight={highlightedDates?.[key]}
|
|
594
615
|
tile={tiles && inMonth && !inBand}
|
|
595
616
|
tint={tint}
|
|
617
|
+
reduce={reduce}
|
|
596
618
|
onPress={onDayPress}
|
|
597
619
|
/>
|
|
598
620
|
);
|
|
@@ -618,6 +640,7 @@ interface DayCellProps {
|
|
|
618
640
|
highlight?: string;
|
|
619
641
|
tile: boolean;
|
|
620
642
|
tint: string;
|
|
643
|
+
reduce: boolean;
|
|
621
644
|
onPress: (d: Date) => void;
|
|
622
645
|
}
|
|
623
646
|
|
|
@@ -636,15 +659,19 @@ const DayCell = memo(function DayCell({
|
|
|
636
659
|
highlight,
|
|
637
660
|
tile,
|
|
638
661
|
tint,
|
|
662
|
+
reduce,
|
|
639
663
|
onPress,
|
|
640
664
|
}: DayCellProps) {
|
|
641
|
-
const { colors, radius } = useKit();
|
|
665
|
+
const { colors, radius, font } = useKit();
|
|
666
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
642
667
|
const r = tileRadius(radius.md);
|
|
643
668
|
const pop = useRef(new Animated.Value(selected ? 1 : 0)).current; // native: circle scale + opacity
|
|
644
669
|
const bandIn = useRef(new Animated.Value(band === 'none' ? 0 : 1)).current; // native: band opacity
|
|
645
670
|
useEffect(() => {
|
|
646
|
-
|
|
647
|
-
|
|
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]);
|
|
648
675
|
useEffect(() => {
|
|
649
676
|
Animated.timing(bandIn, { toValue: band === 'none' ? 0 : 1, duration: motion.normal, useNativeDriver: true }).start();
|
|
650
677
|
}, [band, bandIn]);
|
|
@@ -711,14 +738,16 @@ const DayCell = memo(function DayCell({
|
|
|
711
738
|
borderRadius: r,
|
|
712
739
|
backgroundColor: tint,
|
|
713
740
|
opacity: pop.interpolate({ inputRange: [0, 0.4], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
714
|
-
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] }) }],
|
|
715
742
|
},
|
|
716
743
|
]}
|
|
717
744
|
/>
|
|
718
745
|
<View pointerEvents="none" style={styles.label}>
|
|
719
746
|
<Text
|
|
747
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
720
748
|
style={[
|
|
721
749
|
styles.day,
|
|
750
|
+
ff,
|
|
722
751
|
{ color: textColor },
|
|
723
752
|
(today || selected) && styles.dayStrong,
|
|
724
753
|
outside && !selected && styles.outside,
|
|
@@ -768,11 +797,15 @@ function NavButton({ dir, disabled, onPress }: { dir: 1 | -1; disabled: boolean;
|
|
|
768
797
|
);
|
|
769
798
|
}
|
|
770
799
|
|
|
771
|
-
function Chevron({ turned, color }: { turned: boolean; color: string }) {
|
|
800
|
+
function Chevron({ turned, color, reduce }: { turned: boolean; color: string; reduce: boolean }) {
|
|
772
801
|
const turn = useRef(new Animated.Value(turned ? 1 : 0)).current; // native
|
|
773
802
|
useEffect(() => {
|
|
803
|
+
if (reduce) {
|
|
804
|
+
turn.setValue(turned ? 1 : 0);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
774
807
|
Animated.spring(turn, { toValue: turned ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
775
|
-
}, [turned, turn]);
|
|
808
|
+
}, [turned, turn, reduce]);
|
|
776
809
|
return (
|
|
777
810
|
<Animated.View style={{ transform: [{ rotate: turn.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '180deg'] }) }] }}>
|
|
778
811
|
<Svg width={16} height={16} viewBox="0 0 24 24" fill="none">
|
|
@@ -782,6 +815,22 @@ function Chevron({ turned, color }: { turned: boolean; color: string }) {
|
|
|
782
815
|
);
|
|
783
816
|
}
|
|
784
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
|
+
|
|
785
834
|
const styles = StyleSheet.create({
|
|
786
835
|
root: { alignSelf: 'stretch' },
|
|
787
836
|
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10, minHeight: 40 },
|
|
@@ -789,7 +838,7 @@ const styles = StyleSheet.create({
|
|
|
789
838
|
title: { fontSize: 17, fontWeight: '400', letterSpacing: -0.3, flexShrink: 1 },
|
|
790
839
|
navs: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
|
791
840
|
nav: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center' },
|
|
792
|
-
todayBtn: {
|
|
841
|
+
todayBtn: { minHeight: 32, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center' },
|
|
793
842
|
todayText: { fontSize: 13, fontWeight: '400' },
|
|
794
843
|
weekRow: { flexDirection: 'row', marginBottom: 6 },
|
|
795
844
|
weekday: { flex: 1, textAlign: 'center', fontSize: 13, fontWeight: '400', letterSpacing: 0.2 },
|
|
@@ -817,6 +866,6 @@ const styles = StyleSheet.create({
|
|
|
817
866
|
dot: { width: 5, height: 5, borderRadius: 2.5 },
|
|
818
867
|
pickGrid: { flexDirection: 'row', flexWrap: 'wrap', alignContent: 'center', paddingTop: 4 },
|
|
819
868
|
pickCell: { width: '33.333%', height: 56, alignItems: 'center', justifyContent: 'center' },
|
|
820
|
-
pickInner: { minWidth: 76,
|
|
869
|
+
pickInner: { minWidth: 76, minHeight: 40, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center', borderWidth: 1.5 },
|
|
821
870
|
pickText: { fontSize: 15, fontWeight: '400' },
|
|
822
871
|
});
|