@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,6 @@
|
|
|
1
1
|
import React, { memo, useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
AccessibilityInfo,
|
|
3
4
|
Animated,
|
|
4
5
|
Easing,
|
|
5
6
|
Modal,
|
|
@@ -105,6 +106,10 @@ export interface TimePickerProps {
|
|
|
105
106
|
menuStyle?: StyleProp<ViewStyle>;
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
110
|
+
// content text scales all the way.
|
|
111
|
+
const FONT_CAP = 1.35;
|
|
112
|
+
|
|
108
113
|
const ITEM = 44;
|
|
109
114
|
const VISIBLE = 5;
|
|
110
115
|
const PAD = ITEM * Math.floor(VISIBLE / 2);
|
|
@@ -166,6 +171,7 @@ export function TimePicker({
|
|
|
166
171
|
...rest
|
|
167
172
|
}: TimePickerProps) {
|
|
168
173
|
const [inner, setInner] = useState<TimeValue | null>(defaultValue);
|
|
174
|
+
const reduce = useReduceMotion();
|
|
169
175
|
const current = value !== undefined ? value : inner;
|
|
170
176
|
const h12 = useMemo(() => hour12 ?? uses12Hour(locale), [hour12, locale]);
|
|
171
177
|
const step = Math.max(1, Math.min(60, minuteInterval ?? (variant === 'chips' ? 30 : 1)));
|
|
@@ -175,11 +181,11 @@ export function TimePicker({
|
|
|
175
181
|
};
|
|
176
182
|
|
|
177
183
|
if (presentation === 'sheet') {
|
|
178
|
-
return <TimeField {...rest} variant={variant} hour12={h12} step={step} current={current} commit={commit} />;
|
|
184
|
+
return <TimeField {...rest} variant={variant} hour12={h12} step={step} current={current} commit={commit} reduce={reduce} />;
|
|
179
185
|
}
|
|
180
|
-
if (variant === 'chips') return <SlotGrid {...rest} hour12={h12} step={step} value={current} onPick={commit} />;
|
|
181
|
-
if (variant === 'clock') return <Clock hour12={h12} step={step} value={current} onChange={commit} accent={rest.accent} style={rest.style} />;
|
|
182
|
-
return <Wheel hour12={h12} step={step} value={current} onChange={commit} accent={rest.accent} style={rest.style} />;
|
|
186
|
+
if (variant === 'chips') return <SlotGrid {...rest} hour12={h12} step={step} value={current} onPick={commit} reduce={reduce} />;
|
|
187
|
+
if (variant === 'clock') return <Clock hour12={h12} step={step} value={current} onChange={commit} accent={rest.accent} style={rest.style} reduce={reduce} />;
|
|
188
|
+
return <Wheel hour12={h12} step={step} value={current} onChange={commit} accent={rest.accent} style={rest.style} reduce={reduce} />;
|
|
183
189
|
}
|
|
184
190
|
|
|
185
191
|
/* ───────────────────────────── wheel ───────────────────────────── */
|
|
@@ -191,9 +197,10 @@ interface WheelProps {
|
|
|
191
197
|
onChange: (t: TimeValue) => void;
|
|
192
198
|
accent?: string;
|
|
193
199
|
style?: StyleProp<ViewStyle>;
|
|
200
|
+
reduce: boolean;
|
|
194
201
|
}
|
|
195
202
|
|
|
196
|
-
function Wheel({ hour12, step, value, onChange, accent, style }: WheelProps) {
|
|
203
|
+
function Wheel({ hour12, step, value, onChange, accent, style, reduce }: WheelProps) {
|
|
197
204
|
const { colors, radius, font, dark } = useKit();
|
|
198
205
|
const tint = accent ?? colors.accent;
|
|
199
206
|
const t = value ?? { hours: 9, minutes: 0 };
|
|
@@ -227,13 +234,23 @@ function Wheel({ hour12, step, value, onChange, accent, style }: WheelProps) {
|
|
|
227
234
|
style={[styles.band, { top: PAD, height: ITEM, borderRadius: radius.scale(12), backgroundColor: alpha(tint, dark ? 0.16 : 0.11) }]}
|
|
228
235
|
/>
|
|
229
236
|
<View style={styles.wheelGroup}>
|
|
230
|
-
<WheelColumn items={hours} index={hIdx} onIndexChange={(h) => set({ h })} label="Hour" width={64} align="right" />
|
|
237
|
+
<WheelColumn items={hours} index={hIdx} onIndexChange={(h) => set({ h })} label="Hour" width={64} align="right" reduce={reduce} />
|
|
231
238
|
<View pointerEvents="none" style={styles.colon}>
|
|
232
|
-
<Text style={[styles.colonText, font.family ? { fontFamily: font.family } : null, { color: colors.mutedForeground }]}>:</Text>
|
|
239
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.colonText, font.family ? { fontFamily: font.family } : null, { color: colors.mutedForeground }]}>:</Text>
|
|
233
240
|
</View>
|
|
234
|
-
<WheelColumn items={minutes} index={mIdx} onIndexChange={(m) => set({ m })} label="Minute" width={64} align="left" />
|
|
241
|
+
<WheelColumn items={minutes} index={mIdx} onIndexChange={(m) => set({ m })} label="Minute" width={64} align="left" reduce={reduce} />
|
|
235
242
|
{hour12 ? (
|
|
236
|
-
<WheelColumn
|
|
243
|
+
<WheelColumn
|
|
244
|
+
items={periods}
|
|
245
|
+
index={pIdx}
|
|
246
|
+
onIndexChange={(p) => set({ p })}
|
|
247
|
+
label="AM or PM"
|
|
248
|
+
width={60}
|
|
249
|
+
align="center"
|
|
250
|
+
small
|
|
251
|
+
style={styles.periodCol}
|
|
252
|
+
reduce={reduce}
|
|
253
|
+
/>
|
|
237
254
|
) : null}
|
|
238
255
|
</View>
|
|
239
256
|
</View>
|
|
@@ -249,9 +266,11 @@ interface WheelColumnProps {
|
|
|
249
266
|
align: 'left' | 'right' | 'center';
|
|
250
267
|
small?: boolean;
|
|
251
268
|
style?: StyleProp<ViewStyle>;
|
|
269
|
+
/** Reduce Motion: programmatic scrolls jump; finger scrolling (and its row tilt) is unchanged. */
|
|
270
|
+
reduce: boolean;
|
|
252
271
|
}
|
|
253
272
|
|
|
254
|
-
const WheelColumn = memo(function WheelColumn({ items, index, onIndexChange, label, width, align, small, style }: WheelColumnProps) {
|
|
273
|
+
const WheelColumn = memo(function WheelColumn({ items, index, onIndexChange, label, width, align, small, style, reduce }: WheelColumnProps) {
|
|
255
274
|
const { colors, font } = useKit();
|
|
256
275
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
257
276
|
const scrollY = useRef(new Animated.Value(index * ITEM)).current; // native, driven by the scroll view
|
|
@@ -262,11 +281,14 @@ const WheelColumn = memo(function WheelColumn({ items, index, onIndexChange, lab
|
|
|
262
281
|
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
263
282
|
const cb = useRef(onIndexChange);
|
|
264
283
|
cb.current = onIndexChange;
|
|
284
|
+
// Read by settle(), which also runs from the scroll listener created once.
|
|
285
|
+
const animated = useRef(!reduce);
|
|
286
|
+
animated.current = !reduce;
|
|
265
287
|
|
|
266
288
|
useEffect(() => {
|
|
267
289
|
if (index !== settled.current && !dragging.current) {
|
|
268
290
|
settled.current = index;
|
|
269
|
-
ref.current?.scrollTo({ y: index * ITEM, animated:
|
|
291
|
+
ref.current?.scrollTo({ y: index * ITEM, animated: animated.current });
|
|
270
292
|
}
|
|
271
293
|
}, [index]);
|
|
272
294
|
useEffect(() => () => {
|
|
@@ -276,7 +298,7 @@ const WheelColumn = memo(function WheelColumn({ items, index, onIndexChange, lab
|
|
|
276
298
|
const settle = () => {
|
|
277
299
|
const y = lastY.current;
|
|
278
300
|
const i = Math.max(0, Math.min(items.length - 1, Math.round(y / ITEM)));
|
|
279
|
-
if (Math.abs(y - i * ITEM) > 0.5) ref.current?.scrollTo({ y: i * ITEM, animated:
|
|
301
|
+
if (Math.abs(y - i * ITEM) > 0.5) ref.current?.scrollTo({ y: i * ITEM, animated: animated.current });
|
|
280
302
|
if (i !== settled.current) {
|
|
281
303
|
settled.current = i;
|
|
282
304
|
cb.current(i);
|
|
@@ -289,7 +311,7 @@ const WheelColumn = memo(function WheelColumn({ items, index, onIndexChange, lab
|
|
|
289
311
|
const goTo = (i: number) => {
|
|
290
312
|
const n = Math.max(0, Math.min(items.length - 1, i));
|
|
291
313
|
lastY.current = n * ITEM;
|
|
292
|
-
ref.current?.scrollTo({ y: n * ITEM, animated:
|
|
314
|
+
ref.current?.scrollTo({ y: n * ITEM, animated: !reduce });
|
|
293
315
|
schedule();
|
|
294
316
|
};
|
|
295
317
|
|
|
@@ -360,7 +382,7 @@ const WheelColumn = memo(function WheelColumn({ items, index, onIndexChange, lab
|
|
|
360
382
|
},
|
|
361
383
|
]}
|
|
362
384
|
>
|
|
363
|
-
<Text numberOfLines={1} style={[styles.itemText, size, ff, { color: colors.foreground, textAlign: align }]}>
|
|
385
|
+
<Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.itemText, size, ff, { color: colors.foreground, textAlign: align }]}>
|
|
364
386
|
{item}
|
|
365
387
|
</Text>
|
|
366
388
|
</Animated.View>
|
|
@@ -383,6 +405,7 @@ interface ClockProps {
|
|
|
383
405
|
onChange: (t: TimeValue) => void;
|
|
384
406
|
accent?: string;
|
|
385
407
|
style?: StyleProp<ViewStyle>;
|
|
408
|
+
reduce: boolean;
|
|
386
409
|
}
|
|
387
410
|
|
|
388
411
|
const DIAL_MAX = 280;
|
|
@@ -400,7 +423,7 @@ const handAngle = (t: TimeValue, mode: ClockMode) => (mode === 'hour' ? (t.hours
|
|
|
400
423
|
* hours and minutes, an AM / PM toggle, and a face you drag anywhere on. The hand rotates on the
|
|
401
424
|
* native driver and snaps to hours, then auto-advances to minutes when you let go.
|
|
402
425
|
*/
|
|
403
|
-
function Clock({ hour12, step, value, onChange, accent, style }: ClockProps) {
|
|
426
|
+
function Clock({ hour12, step, value, onChange, accent, style, reduce }: ClockProps) {
|
|
404
427
|
const { colors, radius, font, dark } = useKit();
|
|
405
428
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
406
429
|
const tint = accent ?? colors.accent;
|
|
@@ -426,11 +449,14 @@ function Clock({ hour12, step, value, onChange, accent, style }: ClockProps) {
|
|
|
426
449
|
const target = handAngle(t, mode);
|
|
427
450
|
const d = ((((target - angle.current) % 360) + 540) % 360) - 180;
|
|
428
451
|
angle.current += d;
|
|
429
|
-
|
|
452
|
+
if (reduce) rot.setValue(angle.current);
|
|
453
|
+
else Animated.spring(rot, { toValue: angle.current, useNativeDriver: true, ...motion.snappy }).start();
|
|
430
454
|
}, [t.hours, t.minutes, mode, rot]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
431
455
|
useEffect(() => {
|
|
432
|
-
|
|
433
|
-
|
|
456
|
+
// Reduce Motion: the numbers crossfade without the zoom (scale pinned below).
|
|
457
|
+
const duration = reduce ? 150 : motion.normal - 20;
|
|
458
|
+
Animated.timing(modeAnim, { toValue: mode === 'minute' ? 1 : 0, duration, easing: Easing.out(Easing.cubic), useNativeDriver: true }).start();
|
|
459
|
+
}, [mode, modeAnim, reduce]);
|
|
434
460
|
|
|
435
461
|
// Latest values for the pan responder, which is created once.
|
|
436
462
|
const live = useRef({ t, mode, hour12, step, onChange, c, outer, inner, touched: value !== null });
|
|
@@ -503,7 +529,7 @@ function Clock({ hour12, step, value, onChange, accent, style }: ClockProps) {
|
|
|
503
529
|
const a = (i * Math.PI) / 6;
|
|
504
530
|
return (
|
|
505
531
|
<View key={l} style={[styles.num, { left: c + r * Math.sin(a) - KNOB, top: c - r * Math.cos(a) - KNOB }]}>
|
|
506
|
-
<Text style={[styles.numText, small && styles.numSmall, ff, { color: small ? colors.mutedForeground : colors.foreground }]}>{l}</Text>
|
|
532
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.numText, small && styles.numSmall, ff, { color: small ? colors.mutedForeground : colors.foreground }]}>{l}</Text>
|
|
507
533
|
</View>
|
|
508
534
|
);
|
|
509
535
|
});
|
|
@@ -521,7 +547,7 @@ function Clock({ hour12, step, value, onChange, accent, style }: ClockProps) {
|
|
|
521
547
|
{ width: segW, height: segH, borderRadius: radius.scale(12), backgroundColor: on ? alpha(tint, dark ? 0.22 : 0.14) : colors.muted, opacity: pressed ? 0.75 : 1 },
|
|
522
548
|
]}
|
|
523
549
|
>
|
|
524
|
-
<Text style={[styles.segText, ff, { fontSize: segFont, color: on ? tint : colors.foreground }]}>{text}</Text>
|
|
550
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.segText, ff, { fontSize: segFont, color: on ? tint : colors.foreground }]}>{text}</Text>
|
|
525
551
|
</Pressable>
|
|
526
552
|
);
|
|
527
553
|
};
|
|
@@ -530,7 +556,7 @@ function Clock({ hour12, step, value, onChange, accent, style }: ClockProps) {
|
|
|
530
556
|
<View style={[styles.clock, style]} onLayout={(e) => setWidth(e.nativeEvent.layout.width)}>
|
|
531
557
|
<View style={styles.clockHead}>
|
|
532
558
|
{segment('hour', hourText, 'Hour')}
|
|
533
|
-
<Text style={[styles.clockColon, ff, { fontSize: segFont, color: colors.mutedForeground }]}>:</Text>
|
|
559
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.clockColon, ff, { fontSize: segFont, color: colors.mutedForeground }]}>:</Text>
|
|
534
560
|
{segment('minute', pad2(t.minutes), 'Minute')}
|
|
535
561
|
{hour12 ? (
|
|
536
562
|
<View
|
|
@@ -552,7 +578,7 @@ function Clock({ hour12, step, value, onChange, accent, style }: ClockProps) {
|
|
|
552
578
|
i === 1 && { borderTopWidth: StyleSheet.hairlineWidth * 2, borderTopColor: colors.border },
|
|
553
579
|
]}
|
|
554
580
|
>
|
|
555
|
-
<Text style={[styles.periodText, ff, { color: on ? tint : colors.mutedForeground }]}>{p}</Text>
|
|
581
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.periodText, ff, { color: on ? tint : colors.mutedForeground }]}>{p}</Text>
|
|
556
582
|
</Pressable>
|
|
557
583
|
);
|
|
558
584
|
})}
|
|
@@ -579,14 +605,17 @@ function Clock({ hour12, step, value, onChange, accent, style }: ClockProps) {
|
|
|
579
605
|
<Animated.View
|
|
580
606
|
style={[
|
|
581
607
|
StyleSheet.absoluteFill,
|
|
582
|
-
{
|
|
608
|
+
{
|
|
609
|
+
opacity: modeAnim.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }),
|
|
610
|
+
transform: [{ scale: modeAnim.interpolate({ inputRange: [0, 1], outputRange: [1, reduce ? 1 : 0.88] }) }],
|
|
611
|
+
},
|
|
583
612
|
]}
|
|
584
613
|
>
|
|
585
614
|
{ring(HOUR_OUTER, outer)}
|
|
586
615
|
{hour12 ? null : ring(HOUR_INNER, inner, true)}
|
|
587
616
|
</Animated.View>
|
|
588
617
|
<Animated.View
|
|
589
|
-
style={[StyleSheet.absoluteFill, { opacity: modeAnim, transform: [{ scale: modeAnim.interpolate({ inputRange: [0, 1], outputRange: [1.1, 1] }) }] }]}
|
|
618
|
+
style={[StyleSheet.absoluteFill, { opacity: modeAnim, transform: [{ scale: modeAnim.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 1.1, 1] }) }] }]}
|
|
590
619
|
>
|
|
591
620
|
{ring(MINUTE_LABELS, outer)}
|
|
592
621
|
</Animated.View>
|
|
@@ -599,6 +628,7 @@ function Clock({ hour12, step, value, onChange, accent, style }: ClockProps) {
|
|
|
599
628
|
</Svg>
|
|
600
629
|
<View style={[styles.num, { left: c - KNOB, top: c - handR - KNOB }]}>
|
|
601
630
|
<Animated.Text
|
|
631
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
602
632
|
style={[styles.numText, mode === 'hour' && innerHour && styles.numSmall, ff, { color: ink, transform: [{ rotate: counter }] }]}
|
|
603
633
|
>
|
|
604
634
|
{knobLabel}
|
|
@@ -618,6 +648,7 @@ interface SlotGridProps extends Pick<TimePickerProps, 'startTime' | 'endTime' |
|
|
|
618
648
|
step: number;
|
|
619
649
|
value: TimeValue | null;
|
|
620
650
|
onPick: (t: TimeValue) => void;
|
|
651
|
+
reduce: boolean;
|
|
621
652
|
}
|
|
622
653
|
|
|
623
654
|
function SlotGrid({
|
|
@@ -634,6 +665,7 @@ function SlotGrid({
|
|
|
634
665
|
step,
|
|
635
666
|
value,
|
|
636
667
|
onPick,
|
|
668
|
+
reduce,
|
|
637
669
|
}: SlotGridProps) {
|
|
638
670
|
const { colors, font } = useKit();
|
|
639
671
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
@@ -680,6 +712,7 @@ function SlotGrid({
|
|
|
680
712
|
booked={booked.has(n)}
|
|
681
713
|
disabled={off.has(n)}
|
|
682
714
|
tint={tint}
|
|
715
|
+
reduce={reduce}
|
|
683
716
|
onPick={onPick}
|
|
684
717
|
/>
|
|
685
718
|
</View>
|
|
@@ -699,27 +732,29 @@ interface ChipProps {
|
|
|
699
732
|
booked: boolean;
|
|
700
733
|
disabled: boolean;
|
|
701
734
|
tint: string;
|
|
735
|
+
/** Reduce Motion: no press shrink or select pop; the fill still fades. */
|
|
736
|
+
reduce: boolean;
|
|
702
737
|
onPick: (t: TimeValue) => void;
|
|
703
738
|
}
|
|
704
739
|
|
|
705
|
-
const Chip = memo(function Chip({ minutes, label, selected, booked, disabled, tint, onPick }: ChipProps) {
|
|
740
|
+
const Chip = memo(function Chip({ minutes, label, selected, booked, disabled, tint, reduce, onPick }: ChipProps) {
|
|
706
741
|
const { colors, radius, font } = useKit();
|
|
707
742
|
const pop = useRef(new Animated.Value(1)).current; // native: press + select spring
|
|
708
743
|
const on = useRef(new Animated.Value(selected ? 1 : 0)).current; // native: fill opacity
|
|
709
744
|
useEffect(() => {
|
|
710
745
|
Animated.timing(on, { toValue: selected ? 1 : 0, duration: motion.fast, useNativeDriver: true }).start();
|
|
711
|
-
if (selected) {
|
|
746
|
+
if (selected && !reduce) {
|
|
712
747
|
pop.setValue(0.9);
|
|
713
748
|
Animated.spring(pop, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();
|
|
714
749
|
}
|
|
715
|
-
}, [selected, on, pop]);
|
|
750
|
+
}, [selected, on, pop, reduce]);
|
|
716
751
|
const blocked = booked || disabled;
|
|
717
752
|
|
|
718
753
|
return (
|
|
719
754
|
<Pressable
|
|
720
755
|
onPress={() => onPick(fromMin(minutes))}
|
|
721
|
-
onPressIn={() => Animated.spring(pop, { toValue: 0.95, useNativeDriver: true, ...motion.snappy }).start()}
|
|
722
|
-
onPressOut={() => Animated.spring(pop, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start()}
|
|
756
|
+
onPressIn={reduce ? undefined : () => Animated.spring(pop, { toValue: 0.95, useNativeDriver: true, ...motion.snappy }).start()}
|
|
757
|
+
onPressOut={reduce ? undefined : () => Animated.spring(pop, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start()}
|
|
723
758
|
disabled={blocked}
|
|
724
759
|
accessibilityRole="radio"
|
|
725
760
|
accessibilityLabel={`${label}${booked ? ', booked' : disabled ? ', unavailable' : ''}`}
|
|
@@ -740,6 +775,7 @@ const Chip = memo(function Chip({ minutes, label, selected, booked, disabled, ti
|
|
|
740
775
|
<Animated.View pointerEvents="none" style={[StyleSheet.absoluteFill, { backgroundColor: tint, borderRadius: radius.md - 1, opacity: on }]} />
|
|
741
776
|
<Text
|
|
742
777
|
numberOfLines={1}
|
|
778
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
743
779
|
style={[
|
|
744
780
|
styles.chipText,
|
|
745
781
|
font.family ? { fontFamily: font.family } : null,
|
|
@@ -761,6 +797,7 @@ interface TimeFieldProps extends Omit<TimePickerProps, 'value' | 'defaultValue'
|
|
|
761
797
|
step: number;
|
|
762
798
|
current: TimeValue | null;
|
|
763
799
|
commit: (t: TimeValue) => void;
|
|
800
|
+
reduce: boolean;
|
|
764
801
|
}
|
|
765
802
|
|
|
766
803
|
function TimeField({
|
|
@@ -769,6 +806,7 @@ function TimeField({
|
|
|
769
806
|
step,
|
|
770
807
|
current,
|
|
771
808
|
commit,
|
|
809
|
+
reduce,
|
|
772
810
|
label = 'Time',
|
|
773
811
|
floatingLabel = true,
|
|
774
812
|
title,
|
|
@@ -810,6 +848,9 @@ function TimeField({
|
|
|
810
848
|
const [mounted, setMounted] = useState(false);
|
|
811
849
|
const [draft, setDraft] = useState<TimeValue | null>(current);
|
|
812
850
|
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
851
|
+
// Read by the sheet's pan responder, which is created once.
|
|
852
|
+
const reduceRef = useRef(reduce);
|
|
853
|
+
reduceRef.current = reduce;
|
|
813
854
|
|
|
814
855
|
const ok = !error && !!success;
|
|
815
856
|
const floating = open || !!current;
|
|
@@ -822,26 +863,32 @@ function TimeField({
|
|
|
822
863
|
const check = useRef(new Animated.Value(ok ? 1 : 0)).current; // native
|
|
823
864
|
|
|
824
865
|
useEffect(() => {
|
|
825
|
-
|
|
826
|
-
|
|
866
|
+
if (reduce) float.setValue(floating ? 1 : 0);
|
|
867
|
+
else Animated.spring(float, { toValue: floating ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
868
|
+
}, [floating, float, reduce]);
|
|
827
869
|
useEffect(() => {
|
|
828
870
|
Animated.timing(focus, { toValue: open ? 1 : 0, duration: 180, easing: Easing.out(Easing.quad), useNativeDriver: false }).start();
|
|
829
|
-
|
|
830
|
-
|
|
871
|
+
// Reduce Motion: the halo only fades (its scale is pinned below).
|
|
872
|
+
if (reduce) Animated.timing(halo, { toValue: open ? 1 : 0, duration: 150, useNativeDriver: true }).start();
|
|
873
|
+
else Animated.spring(halo, { toValue: open ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
874
|
+
}, [open, focus, halo, reduce]);
|
|
831
875
|
useEffect(() => {
|
|
832
|
-
Animated.
|
|
833
|
-
|
|
876
|
+
if (reduce) Animated.timing(check, { toValue: ok ? 1 : 0, duration: 150, useNativeDriver: true }).start();
|
|
877
|
+
else Animated.spring(check, { toValue: ok ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
|
|
878
|
+
}, [ok, check, reduce]);
|
|
834
879
|
useEffect(() => {
|
|
835
|
-
if (!error) return;
|
|
880
|
+
if (!error || reduce) return;
|
|
836
881
|
shake.setValue(0);
|
|
837
882
|
Animated.sequence([10, -8, 6, -4, 0].map((x) => Animated.timing(shake, { toValue: x, duration: 55, useNativeDriver: true }))).start();
|
|
838
|
-
}, [error, shake]);
|
|
883
|
+
}, [error, shake, reduce]);
|
|
839
884
|
useEffect(() => {
|
|
840
885
|
if (open) {
|
|
841
886
|
drag.setValue(0);
|
|
842
|
-
|
|
887
|
+
// Reduce Motion: the sheet's slide / scale is pinned below, so `show` only fades it in place.
|
|
888
|
+
if (reduce) Animated.timing(show, { toValue: 1, duration: 150, easing: Easing.out(Easing.cubic), useNativeDriver: true }).start();
|
|
889
|
+
else Animated.spring(show, { toValue: 1, useNativeDriver: true, ...motion.gentle }).start();
|
|
843
890
|
} else if (mounted) {
|
|
844
|
-
Animated.timing(show, { toValue: 0, duration: motion.fast + 40, easing: Easing.in(Easing.quad), useNativeDriver: true }).start(
|
|
891
|
+
Animated.timing(show, { toValue: 0, duration: reduce ? 150 : motion.fast + 40, easing: Easing.in(Easing.quad), useNativeDriver: true }).start(
|
|
845
892
|
({ finished }) => finished && setMounted(false)
|
|
846
893
|
);
|
|
847
894
|
}
|
|
@@ -870,19 +917,21 @@ function TimeField({
|
|
|
870
917
|
}
|
|
871
918
|
};
|
|
872
919
|
|
|
873
|
-
const pan = useMemo(
|
|
874
|
-
() =>
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
920
|
+
const pan = useMemo(() => {
|
|
921
|
+
const back = () =>
|
|
922
|
+
reduceRef.current
|
|
923
|
+
? Animated.timing(drag, { toValue: 0, duration: 150, easing: Easing.bezier(0.32, 0.72, 0, 1), useNativeDriver: true }).start()
|
|
924
|
+
: Animated.spring(drag, { toValue: 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
925
|
+
return PanResponder.create({
|
|
926
|
+
onMoveShouldSetPanResponder: (_, g) => g.dy > 4 && Math.abs(g.dy) > Math.abs(g.dx),
|
|
927
|
+
onPanResponderMove: (_, g) => drag.setValue(Math.max(0, g.dy)),
|
|
928
|
+
onPanResponderRelease: (_, g) => {
|
|
929
|
+
if (g.dy > 110 || g.vy > 1.1) setOpen(false);
|
|
930
|
+
else back();
|
|
931
|
+
},
|
|
932
|
+
onPanResponderTerminate: back,
|
|
933
|
+
});
|
|
934
|
+
}, [drag]);
|
|
886
935
|
|
|
887
936
|
const active = error ? colors.danger : tint;
|
|
888
937
|
const rest = error ? colors.danger : ok ? colors.success : ((userBorder as string | undefined) ?? colors.input);
|
|
@@ -928,13 +977,14 @@ function TimeField({
|
|
|
928
977
|
borderRadius: fieldRadius + 4,
|
|
929
978
|
borderColor: alpha(active, 0.2),
|
|
930
979
|
opacity: halo,
|
|
931
|
-
transform: [{ scale: halo.interpolate({ inputRange: [0, 1], outputRange: [0.97, 1] }) }],
|
|
980
|
+
transform: [{ scale: halo.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.97, 1] }) }],
|
|
932
981
|
},
|
|
933
982
|
]}
|
|
934
983
|
/>
|
|
935
|
-
<Animated.View style={[styles.field, {
|
|
984
|
+
<Animated.View style={[styles.field, { minHeight: sz.h, borderRadius: fieldRadius, backgroundColor: bg }, userField, { borderColor }]}>
|
|
936
985
|
<Text
|
|
937
986
|
numberOfLines={1}
|
|
987
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
938
988
|
style={[
|
|
939
989
|
styles.value,
|
|
940
990
|
ff,
|
|
@@ -962,14 +1012,14 @@ function TimeField({
|
|
|
962
1012
|
]}
|
|
963
1013
|
>
|
|
964
1014
|
{/* The notch: the label paints the surface over the border, so it matches the rendered width. */}
|
|
965
|
-
<Animated.Text numberOfLines={1} style={[styles.label, ff, { fontSize: sz.font, backgroundColor: bg }, labelStyle, { color: labelColor }]}>
|
|
1015
|
+
<Animated.Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.label, ff, { fontSize: sz.font, backgroundColor: bg }, labelStyle, { color: labelColor }]}>
|
|
966
1016
|
{label}
|
|
967
1017
|
</Animated.Text>
|
|
968
1018
|
</Animated.View>
|
|
969
1019
|
) : null}
|
|
970
1020
|
<View pointerEvents="none" style={[styles.adorn, { right: sz.pad - 2 }]}>
|
|
971
1021
|
<Animated.View
|
|
972
|
-
style={{ display: ok ? 'flex' : 'none', opacity: check, transform: [{ scale: check.interpolate({ inputRange: [0, 1], outputRange: [0.3, 1] }) }] }}
|
|
1022
|
+
style={{ display: ok ? 'flex' : 'none', opacity: check, transform: [{ scale: check.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.3, 1] }) }] }}
|
|
973
1023
|
>
|
|
974
1024
|
<Icon name="check" size={sz.icon} color={colors.success} />
|
|
975
1025
|
</Animated.View>
|
|
@@ -977,7 +1027,7 @@ function TimeField({
|
|
|
977
1027
|
</View>
|
|
978
1028
|
</Animated.View>
|
|
979
1029
|
</Pressable>
|
|
980
|
-
{message ? <FieldMessage key={tone + message} text={message} tone={tone} style={messageStyle} /> : null}
|
|
1030
|
+
{message ? <FieldMessage key={tone + message} text={message} tone={tone} reduce={reduce} style={messageStyle} /> : null}
|
|
981
1031
|
|
|
982
1032
|
<Modal visible={mounted} transparent animationType="none" statusBarTranslucent onRequestClose={close}>
|
|
983
1033
|
<Animated.View style={[StyleSheet.absoluteFill, { backgroundColor: colors.overlay, opacity: show }]}>
|
|
@@ -997,11 +1047,15 @@ function TimeField({
|
|
|
997
1047
|
? {
|
|
998
1048
|
opacity: show,
|
|
999
1049
|
transform: [
|
|
1000
|
-
{ scale: show.interpolate({ inputRange: [0, 1], outputRange: [0.94, 1] }) },
|
|
1001
|
-
{ translateY: Animated.add(show.interpolate({ inputRange: [0, 1], outputRange: [16, 0] }), drag) },
|
|
1050
|
+
{ scale: show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.94, 1] }) },
|
|
1051
|
+
{ translateY: Animated.add(show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : 16, 0] }), drag) },
|
|
1002
1052
|
],
|
|
1003
1053
|
}
|
|
1004
|
-
: {
|
|
1054
|
+
: {
|
|
1055
|
+
// Reduce Motion: fade in place instead of sliding up.
|
|
1056
|
+
opacity: reduce ? show : 1,
|
|
1057
|
+
transform: [{ translateY: Animated.add(show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : winH * 0.7, 0] }), drag) }],
|
|
1058
|
+
},
|
|
1005
1059
|
]}
|
|
1006
1060
|
>
|
|
1007
1061
|
<View {...pan.panHandlers} style={styles.sheetHeader}>
|
|
@@ -1013,17 +1067,17 @@ function TimeField({
|
|
|
1013
1067
|
</View>
|
|
1014
1068
|
<ScrollView style={styles.sheetBody} contentContainerStyle={styles.sheetContent} bounces={false} scrollEnabled={variant === 'chips'}>
|
|
1015
1069
|
{variant === 'chips' ? (
|
|
1016
|
-
<SlotGrid {...chipProps} accent={tint} hour12={hour12} step={step} value={draft} onPick={pick} />
|
|
1070
|
+
<SlotGrid {...chipProps} accent={tint} hour12={hour12} step={step} value={draft} onPick={pick} reduce={reduce} />
|
|
1017
1071
|
) : variant === 'clock' ? (
|
|
1018
|
-
<Clock hour12={hour12} step={step} value={draft} onChange={pick} accent={tint} />
|
|
1072
|
+
<Clock hour12={hour12} step={step} value={draft} onChange={pick} accent={tint} reduce={reduce} />
|
|
1019
1073
|
) : (
|
|
1020
|
-
<Wheel hour12={hour12} step={step} value={draft} onChange={pick} accent={tint} />
|
|
1074
|
+
<Wheel hour12={hour12} step={step} value={draft} onChange={pick} accent={tint} reduce={reduce} />
|
|
1021
1075
|
)}
|
|
1022
1076
|
</ScrollView>
|
|
1023
1077
|
{staged ? (
|
|
1024
1078
|
<View style={[styles.footer, { borderTopColor: colors.border }]}>
|
|
1025
1079
|
<Pressable onPress={close} accessibilityRole="button" style={({ pressed }) => [styles.footBtn, { borderRadius: radius.md, opacity: pressed ? 0.6 : 1 }]}>
|
|
1026
|
-
<Text style={[styles.footText, ff, { color: colors.foreground }]}>Cancel</Text>
|
|
1080
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, ff, { color: colors.foreground }]}>Cancel</Text>
|
|
1027
1081
|
</Pressable>
|
|
1028
1082
|
<Pressable
|
|
1029
1083
|
onPress={() => {
|
|
@@ -1038,7 +1092,7 @@ function TimeField({
|
|
|
1038
1092
|
{ backgroundColor: tint, borderRadius: radius.md, opacity: !draft ? 0.4 : pressed ? 0.85 : 1 },
|
|
1039
1093
|
]}
|
|
1040
1094
|
>
|
|
1041
|
-
<Text style={[styles.footText, ff, { color: readableOn(tint) }]}>{draft ? `Set ${formatTime(draft, hour12)}` : 'Done'}</Text>
|
|
1095
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, ff, { color: readableOn(tint) }]}>{draft ? `Set ${formatTime(draft, hour12)}` : 'Done'}</Text>
|
|
1042
1096
|
</Pressable>
|
|
1043
1097
|
</View>
|
|
1044
1098
|
) : null}
|
|
@@ -1050,17 +1104,28 @@ function TimeField({
|
|
|
1050
1104
|
}
|
|
1051
1105
|
|
|
1052
1106
|
/** Helper / error / success line that slides in; error and success carry an icon. */
|
|
1053
|
-
function FieldMessage({
|
|
1107
|
+
function FieldMessage({
|
|
1108
|
+
text,
|
|
1109
|
+
tone,
|
|
1110
|
+
reduce,
|
|
1111
|
+
style,
|
|
1112
|
+
}: {
|
|
1113
|
+
text: string;
|
|
1114
|
+
tone: 'error' | 'success' | 'helper';
|
|
1115
|
+
reduce: boolean;
|
|
1116
|
+
style?: StyleProp<TextStyle>;
|
|
1117
|
+
}) {
|
|
1054
1118
|
const { colors, font } = useKit();
|
|
1055
1119
|
const enter = useRef(new Animated.Value(tone === 'helper' ? 1 : 0)).current; // native
|
|
1056
1120
|
useEffect(() => {
|
|
1057
|
-
Animated.
|
|
1058
|
-
|
|
1121
|
+
if (reduce) Animated.timing(enter, { toValue: 1, duration: 150, useNativeDriver: true }).start();
|
|
1122
|
+
else Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
1123
|
+
}, [enter, reduce]);
|
|
1059
1124
|
const color = tone === 'error' ? colors.danger : tone === 'success' ? colors.success : colors.mutedForeground;
|
|
1060
1125
|
return (
|
|
1061
1126
|
<Animated.View
|
|
1062
1127
|
accessibilityLiveRegion="polite"
|
|
1063
|
-
style={[styles.message, { opacity: enter, transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [4, 0] }) }] }]}
|
|
1128
|
+
style={[styles.message, { opacity: enter, transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : 4, 0] }) }] }]}
|
|
1064
1129
|
>
|
|
1065
1130
|
{tone !== 'helper' ? <Icon name={tone === 'error' ? 'alert' : 'checkCircle'} size={14} color={color} /> : null}
|
|
1066
1131
|
<Text style={[styles.helper, font.family ? { fontFamily: font.family } : null, { color }, style, tone !== 'helper' && { color }]}>{text}</Text>
|
|
@@ -1095,6 +1160,22 @@ function Icon({ name, size, color }: { name: 'clock' | 'check' | 'checkCircle' |
|
|
|
1095
1160
|
);
|
|
1096
1161
|
}
|
|
1097
1162
|
|
|
1163
|
+
function useReduceMotion() {
|
|
1164
|
+
const [reduce, setReduce] = useState(false);
|
|
1165
|
+
useEffect(() => {
|
|
1166
|
+
let alive = true;
|
|
1167
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
1168
|
+
.then((r) => alive && setReduce(r))
|
|
1169
|
+
.catch(() => {});
|
|
1170
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
1171
|
+
return () => {
|
|
1172
|
+
alive = false;
|
|
1173
|
+
sub.remove();
|
|
1174
|
+
};
|
|
1175
|
+
}, []);
|
|
1176
|
+
return reduce;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1098
1179
|
const styles = StyleSheet.create({
|
|
1099
1180
|
wheel: { flexDirection: 'row', justifyContent: 'center', alignSelf: 'stretch', overflow: 'hidden' },
|
|
1100
1181
|
band: { position: 'absolute', left: 8, right: 8 },
|
|
@@ -1119,12 +1200,12 @@ const styles = StyleSheet.create({
|
|
|
1119
1200
|
numSmall: { fontSize: 13 },
|
|
1120
1201
|
slots: { alignSelf: 'stretch', gap: 14 },
|
|
1121
1202
|
slotGroup: { gap: 6 },
|
|
1122
|
-
groupHead: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'baseline', paddingHorizontal: 2 },
|
|
1123
|
-
groupTitle: { fontSize: 13.5, fontWeight: '400' },
|
|
1203
|
+
groupHead: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'baseline', gap: 8, paddingHorizontal: 2 },
|
|
1204
|
+
groupTitle: { flexShrink: 1, fontSize: 13.5, fontWeight: '400' },
|
|
1124
1205
|
groupCount: { fontSize: 12.5, fontWeight: '400' },
|
|
1125
1206
|
grid: { flexDirection: 'row', flexWrap: 'wrap', marginHorizontal: -4 },
|
|
1126
1207
|
chipCell: { padding: 4 },
|
|
1127
|
-
chip: {
|
|
1208
|
+
chip: { minHeight: 44, borderWidth: 1, alignItems: 'center', justifyContent: 'center', overflow: 'hidden' },
|
|
1128
1209
|
chipText: { fontSize: 14.5, fontWeight: '400', fontVariant: ['tabular-nums'] },
|
|
1129
1210
|
struck: { textDecorationLine: 'line-through' },
|
|
1130
1211
|
field: { borderWidth: 1.5, justifyContent: 'center' },
|
|
@@ -1153,7 +1234,7 @@ const styles = StyleSheet.create({
|
|
|
1153
1234
|
sheetBody: { flexGrow: 0, flexShrink: 1 },
|
|
1154
1235
|
sheetContent: { paddingHorizontal: 16, paddingBottom: 12 },
|
|
1155
1236
|
footer: { flexDirection: 'row', gap: 8, paddingTop: 12, paddingHorizontal: 16, borderTopWidth: StyleSheet.hairlineWidth },
|
|
1156
|
-
footBtn: {
|
|
1237
|
+
footBtn: { minHeight: 46, paddingHorizontal: 20, alignItems: 'center', justifyContent: 'center' },
|
|
1157
1238
|
footMain: { flex: 1 },
|
|
1158
1239
|
footText: { fontSize: 15, fontWeight: '400' },
|
|
1159
1240
|
});
|
|
@@ -93,6 +93,10 @@ export interface TimelineProps {
|
|
|
93
93
|
style?: StyleProp<ViewStyle>;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
97
|
+
// content text scales all the way.
|
|
98
|
+
const FONT_CAP = 1.35;
|
|
99
|
+
|
|
96
100
|
const ENTER_STAGGER = 70;
|
|
97
101
|
const FILL_START = 160;
|
|
98
102
|
/** Where the fill point sits in the viewport at the top of the scroll (it slides to the bottom by the end). */
|
|
@@ -479,7 +483,7 @@ function Row({
|
|
|
479
483
|
key={`t-${t}`}
|
|
480
484
|
style={[styles.tag, { borderRadius: radius.capsule(22), backgroundColor: alpha(tint, dark ? 0.2 : 0.1) }]}
|
|
481
485
|
>
|
|
482
|
-
<Text style={[styles.tagText, ff, { color: tint }]}>{t}</Text>
|
|
486
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.tagText, ff, { color: tint }]}>{t}</Text>
|
|
483
487
|
</View>
|
|
484
488
|
))}
|
|
485
489
|
{item.links?.map((l) => (
|
|
@@ -941,7 +945,7 @@ function Photo({ uri, delay, style, more }: { uri: string; delay: number; style:
|
|
|
941
945
|
/>
|
|
942
946
|
{more > 0 ? (
|
|
943
947
|
<View style={[StyleSheet.absoluteFill, styles.more]}>
|
|
944
|
-
<Text style={[styles.moreText, ff]}>+{more}</Text>
|
|
948
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.moreText, ff]}>+{more}</Text>
|
|
945
949
|
</View>
|
|
946
950
|
) : null}
|
|
947
951
|
</View>
|
|
@@ -1039,10 +1043,11 @@ const styles = StyleSheet.create({
|
|
|
1039
1043
|
mini: { position: 'absolute', alignItems: 'center', justifyContent: 'center' },
|
|
1040
1044
|
bodyWrap: { flex: 1, minWidth: 0 },
|
|
1041
1045
|
card: { borderWidth: 1 },
|
|
1042
|
-
|
|
1046
|
+
// Wraps only when the time no longer fits beside the title (large text sizes): it drops below.
|
|
1047
|
+
titleRow: { flexDirection: 'row', flexWrap: 'wrap', alignItems: 'flex-start', gap: 10 },
|
|
1043
1048
|
compactRow: { alignItems: 'center' },
|
|
1044
1049
|
title: { flex: 1, fontWeight: '400', letterSpacing: -0.2 },
|
|
1045
|
-
time: { fontSize: 12.5, fontWeight: '400', fontVariant: ['tabular-nums'] },
|
|
1050
|
+
time: { flexShrink: 1, fontSize: 12.5, fontWeight: '400', fontVariant: ['tabular-nums'] },
|
|
1046
1051
|
desc: { fontSize: 14, fontWeight: '400', lineHeight: 20, marginTop: 3 },
|
|
1047
1052
|
gallery: { marginTop: 12, width: '100%', overflow: 'hidden', borderWidth: 1 },
|
|
1048
1053
|
grid: { flexDirection: 'row', gap: 3 },
|