@expokit/cli 0.4.4 → 0.5.0
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 +30 -3
- package/bin/expo-kit.js +749 -288
- package/bin/lib.mjs +310 -3
- package/package.json +1 -1
- package/registry/_docs/action-sheet.json +1 -1
- package/registry/_docs/alert.json +6 -0
- package/registry/_docs/bottom-sheet.json +6 -0
- package/registry/_docs/bullet.json +6 -0
- package/registry/_docs/dialog.json +1 -1
- package/registry/_docs/drawer.json +3 -3
- package/registry/_docs/glass-search-bar.json +1 -1
- package/registry/_docs/grain.json +6 -1
- package/registry/_docs/isometric-bar.json +6 -0
- package/registry/_docs/liquidglass-tab.json +1 -1
- package/registry/_docs/password-input.json +1 -1
- package/registry/_docs/pixel-bar.json +6 -0
- package/registry/_docs/polar-area.json +6 -0
- package/registry/_docs/qr-code-button.json +9 -3
- package/registry/_docs/scratch-card.json +7 -1
- package/registry/_docs/search-bar.json +1 -1
- package/registry/_docs/select.json +2 -2
- package/registry/_docs/slide-to-confirm.json +2 -2
- package/registry/_docs/spinner.json +1 -1
- package/registry/_docs/stateful-button.json +2 -2
- package/registry/_docs/swipe-deck.json +1 -1
- package/registry/_docs/time-picker.json +2 -2
- package/registry/_docs/timeline-chart.json +6 -0
- package/registry/_docs/toast.json +15 -3
- package/registry/_docs/treemap.json +6 -0
- package/registry/_docs/waterfall.json +6 -0
- package/registry/accordion/files/src/components/ui/accordion.tsx +6 -4
- package/registry/accordion/files/src/lib/kit.tsx +363 -11
- package/registry/accordion.json +1 -0
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +43 -48
- package/registry/action-sheet/files/src/lib/kit.tsx +363 -11
- package/registry/action-sheet.json +1 -0
- package/registry/alert/files/src/components/ui/alert.tsx +19 -12
- package/registry/alert/files/src/lib/kit.tsx +363 -11
- package/registry/alert.json +3 -4
- package/registry/assistant-app.json +1 -0
- package/registry/avatar/files/src/components/ui/avatar.tsx +84 -66
- package/registry/avatar/files/src/lib/kit.tsx +363 -11
- package/registry/avatar.json +1 -0
- package/registry/badge/files/src/components/ui/badge.tsx +34 -34
- package/registry/badge/files/src/lib/kit.tsx +363 -11
- package/registry/badge.json +1 -0
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +28 -27
- package/registry/bottom-nav/files/src/lib/kit.tsx +363 -11
- package/registry/bottom-nav.json +5 -0
- package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +353 -289
- package/registry/bottom-sheet/files/src/lib/kit.tsx +363 -11
- package/registry/bottom-sheet.json +5 -1
- package/registry/bullet.json +1 -0
- package/registry/button/files/src/components/ui/button.tsx +18 -16
- package/registry/button/files/src/lib/kit.tsx +363 -11
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +2 -2
- package/registry/button-3d/files/src/lib/kit.tsx +363 -11
- package/registry/button-3d.json +1 -0
- package/registry/button.json +1 -0
- package/registry/calendar/files/src/components/ui/calendar.tsx +74 -76
- package/registry/calendar/files/src/lib/kit.tsx +363 -11
- package/registry/calendar.json +1 -0
- package/registry/card/files/src/components/ui/card.tsx +3 -3
- package/registry/card/files/src/lib/kit.tsx +363 -11
- package/registry/card-stack.json +1 -0
- package/registry/card.json +1 -0
- package/registry/carousel.json +1 -0
- package/registry/chat.json +7 -0
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +4 -4
- package/registry/checkbox/files/src/lib/kit.tsx +363 -11
- package/registry/checkbox.json +1 -0
- package/registry/checkout.json +1 -0
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +22 -22
- package/registry/chip-group/files/src/lib/kit.tsx +363 -11
- package/registry/chip-group.json +1 -0
- package/registry/confetti/files/src/components/ui/confetti.tsx +15 -14
- package/registry/confetti.json +3 -6
- package/registry/coupon/files/src/components/ui/coupon.tsx +42 -42
- package/registry/coupon/files/src/components/ui/flip-card.tsx +6 -5
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +76 -91
- package/registry/coupon/files/src/lib/kit.tsx +363 -11
- package/registry/coupon/files/src/lib/ticket-shape.tsx +54 -44
- package/registry/coupon.json +1 -0
- package/registry/cube-pager.json +1 -0
- package/registry/dashboard.json +1 -0
- package/registry/date-picker/files/src/components/ui/calendar.tsx +74 -76
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +55 -56
- package/registry/date-picker/files/src/lib/kit.tsx +363 -11
- package/registry/date-picker.json +1 -0
- package/registry/dialog/files/src/components/ui/dialog.tsx +60 -56
- package/registry/dialog/files/src/lib/kit.tsx +363 -11
- package/registry/dialog.json +1 -0
- package/registry/donut-shop.json +1 -0
- package/registry/drawer/files/src/components/ui/drawer.tsx +73 -69
- package/registry/drawer/files/src/lib/kit.tsx +363 -11
- package/registry/drawer.json +1 -0
- package/registry/fab.json +1 -0
- package/registry/fintech-app.json +1 -0
- package/registry/flip-calendar/files/src/components/ui/flip-calendar.tsx +92 -86
- package/registry/flip-calendar/files/src/lib/kit.tsx +363 -11
- package/registry/flip-calendar.json +7 -0
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +6 -5
- package/registry/flip-card/files/src/lib/kit.tsx +363 -11
- package/registry/flip-card.json +1 -0
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +33 -33
- package/registry/floating-input/files/src/lib/kit.tsx +363 -11
- package/registry/floating-input.json +1 -0
- package/registry/food-app.json +1 -0
- package/registry/gift-card.json +1 -0
- package/registry/glass-button.json +5 -0
- package/registry/glass-nav-bar.json +1 -0
- package/registry/glass-search-bar.json +1 -0
- package/registry/glass-segmented.json +1 -0
- package/registry/glass-sheet.json +1 -0
- package/registry/glass-tab-bar.json +5 -0
- package/registry/glass.json +1 -0
- package/registry/grain/files/src/components/ui/grain.tsx +16 -14
- package/registry/grain.json +8 -6
- package/registry/inbox.json +1 -0
- package/registry/isometric-bar.json +1 -0
- package/registry/kit/files/src/lib/kit.tsx +363 -11
- package/registry/kit.json +1 -0
- package/registry/liquidglass-tab/files/src/app/_layout.tsx +0 -1
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +23 -9
- package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +2 -2
- package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +1 -1
- package/registry/liquidglass-tab.json +1 -0
- package/registry/list-item/files/src/components/ui/list-item.tsx +42 -42
- package/registry/list-item/files/src/lib/kit.tsx +363 -11
- package/registry/list-item.json +1 -0
- package/registry/loyalty-card.json +1 -0
- package/registry/morphing-button-group.json +1 -0
- package/registry/number-ticker.json +1 -0
- package/registry/offers.json +1 -0
- package/registry/onboarding.json +1 -0
- package/registry/orb-reveal.json +1 -0
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +48 -29
- package/registry/otp-input/files/src/lib/kit.tsx +363 -11
- package/registry/otp-input.json +1 -0
- package/registry/parallax-layers.json +1 -0
- package/registry/parallax-scroll.json +1 -0
- package/registry/password-input/files/src/components/ui/floating-input.tsx +33 -33
- package/registry/password-input/files/src/components/ui/password-input.tsx +29 -25
- package/registry/password-input/files/src/lib/kit.tsx +363 -11
- package/registry/password-input.json +1 -0
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +6 -5
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +17 -17
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +76 -91
- package/registry/payment-card/files/src/lib/kit.tsx +363 -11
- package/registry/payment-card.json +1 -0
- package/registry/paywall.json +1 -0
- package/registry/pixel-bar.json +1 -0
- package/registry/polar-area.json +1 -0
- package/registry/profile.json +1 -0
- package/registry/progress/files/src/components/ui/progress.tsx +29 -29
- package/registry/progress/files/src/lib/kit.tsx +363 -11
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +20 -20
- package/registry/progress-steps/files/src/lib/kit.tsx +363 -11
- package/registry/progress-steps.json +1 -0
- package/registry/progress.json +1 -0
- package/registry/qr-code-button.json +1 -0
- package/registry/radio-group/files/src/components/ui/radio-group.tsx +19 -19
- package/registry/radio-group/files/src/lib/kit.tsx +363 -11
- package/registry/radio-group.json +1 -0
- package/registry/rating.json +1 -0
- package/registry/ring-carousel.json +1 -0
- package/registry/ruler-time-picker/files/src/components/ui/ruler-time-picker.tsx +98 -82
- package/registry/ruler-time-picker/files/src/lib/kit.tsx +363 -11
- package/registry/ruler-time-picker.json +7 -0
- package/registry/scratch-card.json +1 -0
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +25 -21
- package/registry/search-bar/files/src/lib/kit.tsx +363 -11
- package/registry/search-bar.json +1 -0
- package/registry/search-results.json +4 -0
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +8 -7
- package/registry/segmented-control/files/src/lib/kit.tsx +363 -11
- package/registry/segmented-control.json +1 -0
- package/registry/select/files/src/components/ui/select.tsx +93 -88
- package/registry/select/files/src/lib/kit.tsx +363 -11
- package/registry/select.json +6 -0
- package/registry/settings.json +1 -0
- package/registry/sign-in.json +1 -0
- package/registry/skeleton/files/src/components/ui/skeleton.tsx +149 -80
- package/registry/skeleton/files/src/lib/kit.tsx +363 -11
- package/registry/skeleton.json +1 -0
- package/registry/slide-to-confirm.json +1 -0
- package/registry/slider/files/src/components/ui/slider.tsx +103 -96
- package/registry/slider/files/src/lib/kit.tsx +363 -11
- package/registry/slider.json +5 -0
- package/registry/spin-wheel.json +1 -0
- package/registry/spinner/files/src/components/ui/spinner.tsx +39 -29
- package/registry/spinner/files/src/lib/kit.tsx +363 -11
- package/registry/spinner.json +6 -0
- package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +39 -32
- package/registry/stateful-button/files/src/lib/kit.tsx +363 -11
- package/registry/stateful-button.json +5 -0
- package/registry/stepper/files/src/components/ui/stepper.tsx +38 -36
- package/registry/stepper/files/src/lib/kit.tsx +363 -11
- package/registry/stepper.json +1 -0
- package/registry/swipe-deck.json +1 -0
- package/registry/swipeable-row.json +4 -0
- package/registry/switch/files/src/components/ui/switch.tsx +39 -42
- package/registry/switch/files/src/lib/kit.tsx +363 -11
- package/registry/switch.json +1 -0
- package/registry/tabs/files/src/components/ui/tabs.tsx +59 -62
- package/registry/tabs/files/src/lib/kit.tsx +363 -11
- package/registry/tabs.json +1 -0
- package/registry/text-animation/files/src/components/ui/text-animation.tsx +55 -42
- package/registry/text-animation/files/src/lib/kit.tsx +363 -11
- package/registry/text-animation.json +7 -0
- package/registry/textarea/files/src/components/ui/textarea.tsx +10 -10
- package/registry/textarea/files/src/lib/kit.tsx +363 -11
- package/registry/textarea.json +1 -0
- package/registry/ticket-card.json +1 -0
- package/registry/ticket-shape/files/src/lib/kit.tsx +363 -11
- package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +54 -44
- package/registry/ticket-shape.json +1 -0
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +76 -91
- package/registry/tilt-card/files/src/lib/kit.tsx +363 -11
- package/registry/tilt-card.json +1 -0
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +122 -119
- package/registry/time-picker/files/src/lib/kit.tsx +363 -11
- package/registry/time-picker.json +1 -0
- package/registry/timeline/files/src/components/ui/avatar.tsx +84 -66
- package/registry/timeline/files/src/components/ui/timeline.tsx +44 -41
- package/registry/timeline/files/src/lib/kit.tsx +363 -11
- package/registry/timeline-chart.json +1 -0
- package/registry/timeline.json +1 -0
- package/registry/toast/files/src/components/ui/toast.tsx +541 -463
- package/registry/toast/files/src/lib/kit.tsx +363 -11
- package/registry/toast.json +7 -5
- package/registry/treemap.json +1 -0
- package/registry/waterfall.json +1 -0
- package/registry/wheel-list.json +1 -0
- package/schema/components.schema.json +1 -0
- package/registry/alert/files/src/components/ui/button.tsx +0 -290
- package/registry/confetti/files/src/lib/kit.tsx +0 -320
- package/registry/grain/files/src/lib/kit.tsx +0 -320
- package/registry/toast/files/src/components/ui/button.tsx +0 -290
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import React, { memo, useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
|
|
1
|
+
import React, { memo, useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
AccessibilityInfo,
|
|
4
4
|
Animated,
|
|
5
5
|
Easing,
|
|
6
6
|
Modal,
|
|
7
|
-
PanResponder,
|
|
8
7
|
Pressable,
|
|
9
8
|
ScrollView,
|
|
10
9
|
StyleSheet,
|
|
@@ -18,7 +17,7 @@ import {
|
|
|
18
17
|
} from 'react-native';
|
|
19
18
|
import Svg, { Circle, Line, Path } from 'react-native-svg';
|
|
20
19
|
|
|
21
|
-
import { alpha, motion, readableOn, useKit, useLayout } from '@/lib/kit';
|
|
20
|
+
import { alpha, motion, readableOn, useKit, useLayout, usePanResponder } from '@/lib/kit';
|
|
22
21
|
|
|
23
22
|
/** A wall-clock time, 24-hour: `{ hours: 0–23, minutes: 0–59 }`. */
|
|
24
23
|
export interface TimeValue {
|
|
@@ -143,11 +142,11 @@ export function uses12Hour(locale?: string) {
|
|
|
143
142
|
}
|
|
144
143
|
}
|
|
145
144
|
|
|
146
|
-
/** '9:30 AM' or '09:30'. */
|
|
147
|
-
export function formatTime(t: TimeValue, hour12 = true) {
|
|
145
|
+
/** '9:30 AM' or '09:30'. `periods` names AM / PM (default English; pass `useKit().labels` to localise). */
|
|
146
|
+
export function formatTime(t: TimeValue, hour12 = true, periods: { am: string; pm: string } = { am: 'AM', pm: 'PM' }) {
|
|
148
147
|
if (!hour12) return `${pad2(t.hours)}:${pad2(t.minutes)}`;
|
|
149
148
|
const h = t.hours % 12 || 12;
|
|
150
|
-
return `${h}:${pad2(t.minutes)} ${t.hours < 12 ?
|
|
149
|
+
return `${h}:${pad2(t.minutes)} ${t.hours < 12 ? periods.am : periods.pm}`;
|
|
151
150
|
}
|
|
152
151
|
|
|
153
152
|
/* ───────────────────────────── TimePicker ───────────────────────────── */
|
|
@@ -194,18 +193,18 @@ interface WheelProps {
|
|
|
194
193
|
step: number;
|
|
195
194
|
value: TimeValue | null;
|
|
196
195
|
onChange: (t: TimeValue) => void;
|
|
197
|
-
accent?: string;
|
|
198
|
-
style?: StyleProp<ViewStyle
|
|
196
|
+
accent?: string | undefined;
|
|
197
|
+
style?: StyleProp<ViewStyle> | undefined;
|
|
199
198
|
reduce: boolean;
|
|
200
199
|
}
|
|
201
200
|
|
|
202
201
|
function Wheel({ hour12, step, value, onChange, accent, style, reduce }: WheelProps) {
|
|
203
|
-
const { colors, radius, font, dark } = useKit();
|
|
202
|
+
const { colors, radius, font, dark, labels } = useKit();
|
|
204
203
|
const tint = accent ?? colors.accent;
|
|
205
204
|
const t = value ?? { hours: 9, minutes: 0 };
|
|
206
205
|
const hours = useMemo(() => (hour12 ? ['12', ...Array.from({ length: 11 }, (_, i) => String(i + 1))] : Array.from({ length: 24 }, (_, i) => pad2(i))), [hour12]);
|
|
207
206
|
const minutes = useMemo(() => Array.from({ length: Math.ceil(60 / step) }, (_, i) => pad2(i * step)), [step]);
|
|
208
|
-
const periods = useMemo(() => [
|
|
207
|
+
const periods = useMemo(() => [labels.am, labels.pm], [labels.am, labels.pm]);
|
|
209
208
|
|
|
210
209
|
const hIdx = hour12 ? t.hours % 12 : t.hours;
|
|
211
210
|
const mIdx = Math.min(minutes.length - 1, Math.round(t.minutes / step));
|
|
@@ -213,7 +212,9 @@ function Wheel({ hour12, step, value, onChange, accent, style, reduce }: WheelPr
|
|
|
213
212
|
|
|
214
213
|
// Keep the latest value for column callbacks that fire after a scroll settles.
|
|
215
214
|
const latest = useRef(t);
|
|
216
|
-
|
|
215
|
+
useLayoutEffect(() => {
|
|
216
|
+
latest.current = t;
|
|
217
|
+
});
|
|
217
218
|
const set = (patch: Partial<{ h: number; m: number; p: number }>) => {
|
|
218
219
|
const cur = latest.current;
|
|
219
220
|
const curP = cur.hours >= 12 ? 1 : 0;
|
|
@@ -233,17 +234,17 @@ function Wheel({ hour12, step, value, onChange, accent, style, reduce }: WheelPr
|
|
|
233
234
|
style={[styles.band, { top: PAD, height: ITEM, borderRadius: radius.scale(12), backgroundColor: alpha(tint, dark ? 0.16 : 0.11) }]}
|
|
234
235
|
/>
|
|
235
236
|
<View style={styles.wheelGroup}>
|
|
236
|
-
<WheelColumn items={hours} index={hIdx} onIndexChange={(h) => set({ h })} label=
|
|
237
|
+
<WheelColumn items={hours} index={hIdx} onIndexChange={(h) => set({ h })} label={labels.hour} width={64} align="right" reduce={reduce} />
|
|
237
238
|
<View pointerEvents="none" style={styles.colon}>
|
|
238
239
|
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.colonText, font.family ? { fontFamily: font.family } : null, { color: colors.mutedForeground }]}>:</Text>
|
|
239
240
|
</View>
|
|
240
|
-
<WheelColumn items={minutes} index={mIdx} onIndexChange={(m) => set({ m })} label=
|
|
241
|
+
<WheelColumn items={minutes} index={mIdx} onIndexChange={(m) => set({ m })} label={labels.minute} width={64} align="left" reduce={reduce} />
|
|
241
242
|
{hour12 ? (
|
|
242
243
|
<WheelColumn
|
|
243
244
|
items={periods}
|
|
244
245
|
index={pIdx}
|
|
245
246
|
onIndexChange={(p) => set({ p })}
|
|
246
|
-
label=
|
|
247
|
+
label={labels.amPm}
|
|
247
248
|
width={60}
|
|
248
249
|
align="center"
|
|
249
250
|
small
|
|
@@ -272,17 +273,21 @@ interface WheelColumnProps {
|
|
|
272
273
|
const WheelColumn = memo(function WheelColumn({ items, index, onIndexChange, label, width, align, small, style, reduce }: WheelColumnProps) {
|
|
273
274
|
const { colors, font } = useKit();
|
|
274
275
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
275
|
-
const scrollY =
|
|
276
|
+
const [scrollY] = useState(() => new Animated.Value(index * ITEM)); // native, driven by the scroll view
|
|
276
277
|
const ref = useRef<ComponentRef<typeof ScrollView>>(null);
|
|
277
278
|
const settled = useRef(index);
|
|
278
279
|
const dragging = useRef(false);
|
|
279
280
|
const lastY = useRef(index * ITEM);
|
|
280
281
|
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
281
282
|
const cb = useRef(onIndexChange);
|
|
282
|
-
|
|
283
|
+
useLayoutEffect(() => {
|
|
284
|
+
cb.current = onIndexChange;
|
|
285
|
+
});
|
|
283
286
|
// Read by settle(), which also runs from the scroll listener created once.
|
|
284
287
|
const animated = useRef(!reduce);
|
|
285
|
-
|
|
288
|
+
useLayoutEffect(() => {
|
|
289
|
+
animated.current = !reduce;
|
|
290
|
+
});
|
|
286
291
|
|
|
287
292
|
useEffect(() => {
|
|
288
293
|
if (index !== settled.current && !dragging.current) {
|
|
@@ -316,6 +321,7 @@ const WheelColumn = memo(function WheelColumn({ items, index, onIndexChange, lab
|
|
|
316
321
|
|
|
317
322
|
const onScroll = useMemo(
|
|
318
323
|
() =>
|
|
324
|
+
// eslint-disable-next-line react-hooks/refs -- the listener reads refs on scroll events, never during render
|
|
319
325
|
Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], {
|
|
320
326
|
useNativeDriver: true,
|
|
321
327
|
listener: (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
@@ -402,8 +408,8 @@ interface ClockProps {
|
|
|
402
408
|
step: number;
|
|
403
409
|
value: TimeValue | null;
|
|
404
410
|
onChange: (t: TimeValue) => void;
|
|
405
|
-
accent?: string;
|
|
406
|
-
style?: StyleProp<ViewStyle
|
|
411
|
+
accent?: string | undefined;
|
|
412
|
+
style?: StyleProp<ViewStyle> | undefined;
|
|
407
413
|
reduce: boolean;
|
|
408
414
|
}
|
|
409
415
|
|
|
@@ -423,7 +429,7 @@ const handAngle = (t: TimeValue, mode: ClockMode) => (mode === 'hour' ? (t.hours
|
|
|
423
429
|
* native driver and snaps to hours, then auto-advances to minutes when you let go.
|
|
424
430
|
*/
|
|
425
431
|
function Clock({ hour12, step, value, onChange, accent, style, reduce }: ClockProps) {
|
|
426
|
-
const { colors, radius, font, dark } = useKit();
|
|
432
|
+
const { colors, radius, font, dark, labels } = useKit();
|
|
427
433
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
428
434
|
const tint = accent ?? colors.accent;
|
|
429
435
|
const ink = readableOn(tint);
|
|
@@ -441,9 +447,9 @@ function Clock({ hour12, step, value, onChange, accent, style, reduce }: ClockPr
|
|
|
441
447
|
const handR = mode === 'hour' && innerHour ? inner : outer;
|
|
442
448
|
|
|
443
449
|
// Hand rotation in degrees (native). Kept continuous so 55 → 00 turns forward, not all the way back.
|
|
444
|
-
const rot =
|
|
450
|
+
const [rot] = useState(() => new Animated.Value(handAngle(t, 'hour')));
|
|
445
451
|
const angle = useRef(handAngle(t, 'hour'));
|
|
446
|
-
const modeAnim =
|
|
452
|
+
const [modeAnim] = useState(() => new Animated.Value(0)); // native: number crossfade
|
|
447
453
|
useEffect(() => {
|
|
448
454
|
const target = handAngle(t, mode);
|
|
449
455
|
const d = ((((target - angle.current) % 360) + 540) % 360) - 180;
|
|
@@ -459,48 +465,48 @@ function Clock({ hour12, step, value, onChange, accent, style, reduce }: ClockPr
|
|
|
459
465
|
|
|
460
466
|
// Latest values for the pan responder, which is created once.
|
|
461
467
|
const live = useRef({ t, mode, hour12, step, onChange, c, outer, inner, touched: value !== null });
|
|
462
|
-
|
|
468
|
+
useLayoutEffect(() => {
|
|
469
|
+
live.current = { t, mode, hour12, step, onChange, c, outer, inner, touched: value !== null };
|
|
470
|
+
});
|
|
463
471
|
const origin = useRef({ x: 0, y: 0 });
|
|
464
472
|
|
|
465
|
-
const
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
next = { hours: innerRing ? (i === 0 ? 0 : i + 12) : i === 0 ? 12 : i, minutes: cur.minutes };
|
|
480
|
-
}
|
|
481
|
-
} else {
|
|
482
|
-
next = { hours: cur.hours, minutes: (Math.round(deg / 6 / L.step) * L.step) % 60 };
|
|
473
|
+
const pick = (x: number, y: number) => {
|
|
474
|
+
const L = live.current;
|
|
475
|
+
const dx = x - L.c;
|
|
476
|
+
const dy = y - L.c;
|
|
477
|
+
let deg = (Math.atan2(dx, -dy) * 180) / Math.PI;
|
|
478
|
+
if (deg < 0) deg += 360;
|
|
479
|
+
const cur = L.t;
|
|
480
|
+
let next: TimeValue;
|
|
481
|
+
if (L.mode === 'hour') {
|
|
482
|
+
const i = Math.round(deg / 30) % 12;
|
|
483
|
+
if (L.hour12) next = { hours: i + (cur.hours >= 12 ? 12 : 0), minutes: cur.minutes };
|
|
484
|
+
else {
|
|
485
|
+
const innerRing = Math.hypot(dx, dy) < (L.outer + L.inner) / 2;
|
|
486
|
+
next = { hours: innerRing ? (i === 0 ? 0 : i + 12) : i === 0 ? 12 : i, minutes: cur.minutes };
|
|
483
487
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
488
|
+
} else {
|
|
489
|
+
next = { hours: cur.hours, minutes: (Math.round(deg / 6 / L.step) * L.step) % 60 };
|
|
490
|
+
}
|
|
491
|
+
if (next.hours !== cur.hours || next.minutes !== cur.minutes || !L.touched) {
|
|
492
|
+
L.t = next;
|
|
493
|
+
L.touched = true;
|
|
494
|
+
L.onChange(next);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
const pan = usePanResponder({
|
|
498
|
+
onStartShouldSetPanResponder: () => true,
|
|
499
|
+
onMoveShouldSetPanResponder: () => true,
|
|
500
|
+
onPanResponderTerminationRequest: () => false,
|
|
501
|
+
onPanResponderGrant: (e) => {
|
|
502
|
+
origin.current = { x: e.nativeEvent.locationX, y: e.nativeEvent.locationY };
|
|
503
|
+
pick(origin.current.x, origin.current.y);
|
|
504
|
+
},
|
|
505
|
+
onPanResponderMove: (_, g) => pick(origin.current.x + g.dx, origin.current.y + g.dy),
|
|
506
|
+
onPanResponderRelease: () => {
|
|
507
|
+
if (live.current.mode === 'hour') setMode('minute');
|
|
508
|
+
},
|
|
509
|
+
});
|
|
504
510
|
|
|
505
511
|
const adjust = (dir: 1 | -1) => {
|
|
506
512
|
if (mode === 'hour') {
|
|
@@ -554,22 +560,22 @@ function Clock({ hour12, step, value, onChange, accent, style, reduce }: ClockPr
|
|
|
554
560
|
return (
|
|
555
561
|
<View style={[styles.clock, style]} onLayout={(e) => setWidth(e.nativeEvent.layout.width)}>
|
|
556
562
|
<View style={styles.clockHead}>
|
|
557
|
-
{segment('hour', hourText,
|
|
563
|
+
{segment('hour', hourText, labels.hour)}
|
|
558
564
|
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.clockColon, ff, { fontSize: segFont, color: colors.mutedForeground }]}>:</Text>
|
|
559
|
-
{segment('minute', pad2(t.minutes),
|
|
565
|
+
{segment('minute', pad2(t.minutes), labels.minute)}
|
|
560
566
|
{hour12 ? (
|
|
561
567
|
<View
|
|
562
568
|
accessibilityRole="radiogroup"
|
|
563
569
|
style={[styles.period, { height: segH, borderRadius: radius.scale(10), borderColor: colors.border }]}
|
|
564
570
|
>
|
|
565
|
-
{(['
|
|
566
|
-
const on = (p === '
|
|
571
|
+
{(['am', 'pm'] as const).map((p, i) => {
|
|
572
|
+
const on = (p === 'pm') === pm;
|
|
567
573
|
return (
|
|
568
574
|
<Pressable
|
|
569
575
|
key={p}
|
|
570
|
-
onPress={() => setPeriod(p === '
|
|
576
|
+
onPress={() => setPeriod(p === 'pm')}
|
|
571
577
|
accessibilityRole="radio"
|
|
572
|
-
accessibilityLabel={p}
|
|
578
|
+
accessibilityLabel={labels[p]}
|
|
573
579
|
accessibilityState={{ checked: on }}
|
|
574
580
|
style={[
|
|
575
581
|
styles.periodBtn,
|
|
@@ -577,7 +583,7 @@ function Clock({ hour12, step, value, onChange, accent, style, reduce }: ClockPr
|
|
|
577
583
|
i === 1 && { borderTopWidth: StyleSheet.hairlineWidth * 2, borderTopColor: colors.border },
|
|
578
584
|
]}
|
|
579
585
|
>
|
|
580
|
-
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.periodText, ff, { color: on ? tint : colors.mutedForeground }]}>{p}</Text>
|
|
586
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.periodText, ff, { color: on ? tint : colors.mutedForeground }]}>{labels[p]}</Text>
|
|
581
587
|
</Pressable>
|
|
582
588
|
);
|
|
583
589
|
})}
|
|
@@ -586,12 +592,12 @@ function Clock({ hour12, step, value, onChange, accent, style, reduce }: ClockPr
|
|
|
586
592
|
</View>
|
|
587
593
|
|
|
588
594
|
<View
|
|
589
|
-
{...pan
|
|
595
|
+
{...pan}
|
|
590
596
|
accessible
|
|
591
597
|
accessibilityRole="adjustable"
|
|
592
|
-
accessibilityLabel={mode === 'hour' ?
|
|
593
|
-
accessibilityHint=
|
|
594
|
-
accessibilityValue={{ text: mode === 'hour' ? `${knobLabel || hourText} ${
|
|
598
|
+
accessibilityLabel={mode === 'hour' ? labels.hour : labels.minute}
|
|
599
|
+
accessibilityHint={labels.timeDialHint}
|
|
600
|
+
accessibilityValue={{ text: mode === 'hour' ? (hour12 ? `${knobLabel || hourText} ${pm ? labels.pm : labels.am}` : labels.hours(knobLabel || hourText)) : labels.minutes(t.minutes) }}
|
|
595
601
|
accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
|
|
596
602
|
onAccessibilityAction={(e) => adjust(e.nativeEvent.actionName === 'increment' ? 1 : -1)}
|
|
597
603
|
style={[styles.face, { width: size, height: size }]}
|
|
@@ -666,7 +672,7 @@ function SlotGrid({
|
|
|
666
672
|
onPick,
|
|
667
673
|
reduce,
|
|
668
674
|
}: SlotGridProps) {
|
|
669
|
-
const { colors, font } = useKit();
|
|
675
|
+
const { colors, font, labels } = useKit();
|
|
670
676
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
671
677
|
const tint = accent ?? colors.accent;
|
|
672
678
|
const list = useMemo(() => {
|
|
@@ -682,12 +688,12 @@ function SlotGrid({
|
|
|
682
688
|
const groups = useMemo(() => {
|
|
683
689
|
if (!groupByPeriod) return [{ title: '', items: list }];
|
|
684
690
|
const g = [
|
|
685
|
-
{ title:
|
|
686
|
-
{ title:
|
|
687
|
-
{ title:
|
|
691
|
+
{ title: labels.morning, items: list.filter((n) => n < 720) },
|
|
692
|
+
{ title: labels.afternoon, items: list.filter((n) => n >= 720 && n < 1020) },
|
|
693
|
+
{ title: labels.evening, items: list.filter((n) => n >= 1020) },
|
|
688
694
|
];
|
|
689
695
|
return g.filter((x) => x.items.length);
|
|
690
|
-
}, [list, groupByPeriod]);
|
|
696
|
+
}, [list, groupByPeriod, labels]);
|
|
691
697
|
|
|
692
698
|
return (
|
|
693
699
|
<View style={[styles.slots, style]} accessibilityRole="radiogroup">
|
|
@@ -698,7 +704,7 @@ function SlotGrid({
|
|
|
698
704
|
{g.title ? (
|
|
699
705
|
<View style={styles.groupHead}>
|
|
700
706
|
<Text style={[styles.groupTitle, ff, { color: colors.foreground }]}>{g.title}</Text>
|
|
701
|
-
<Text style={[styles.groupCount, ff, { color: colors.mutedForeground }]}>{free ?
|
|
707
|
+
<Text style={[styles.groupCount, ff, { color: colors.mutedForeground }]}>{free ? labels.slotsOpen(free) : labels.full}</Text>
|
|
702
708
|
</View>
|
|
703
709
|
) : null}
|
|
704
710
|
<View style={styles.grid}>
|
|
@@ -706,7 +712,7 @@ function SlotGrid({
|
|
|
706
712
|
<View key={n} style={[styles.chipCell, { width: `${100 / columns}%` }]}>
|
|
707
713
|
<Chip
|
|
708
714
|
minutes={n}
|
|
709
|
-
label={formatTime(fromMin(n), hour12)}
|
|
715
|
+
label={formatTime(fromMin(n), hour12, labels)}
|
|
710
716
|
selected={n === selected}
|
|
711
717
|
booked={booked.has(n)}
|
|
712
718
|
disabled={off.has(n)}
|
|
@@ -737,9 +743,9 @@ interface ChipProps {
|
|
|
737
743
|
}
|
|
738
744
|
|
|
739
745
|
const Chip = memo(function Chip({ minutes, label, selected, booked, disabled, tint, reduce, onPick }: ChipProps) {
|
|
740
|
-
const { colors, radius, font } = useKit();
|
|
741
|
-
const pop =
|
|
742
|
-
const on =
|
|
746
|
+
const { colors, radius, font, labels } = useKit();
|
|
747
|
+
const [pop] = useState(() => new Animated.Value(1)); // native: press + select spring
|
|
748
|
+
const [on] = useState(() => new Animated.Value(selected ? 1 : 0)); // native: fill opacity
|
|
743
749
|
useEffect(() => {
|
|
744
750
|
Animated.timing(on, { toValue: selected ? 1 : 0, duration: motion.fast, useNativeDriver: true }).start();
|
|
745
751
|
if (selected && !reduce) {
|
|
@@ -756,7 +762,7 @@ const Chip = memo(function Chip({ minutes, label, selected, booked, disabled, ti
|
|
|
756
762
|
onPressOut={reduce ? undefined : () => Animated.spring(pop, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start()}
|
|
757
763
|
disabled={blocked}
|
|
758
764
|
accessibilityRole="radio"
|
|
759
|
-
accessibilityLabel={`${label}${booked ?
|
|
765
|
+
accessibilityLabel={`${label}${booked ? `, ${labels.booked}` : disabled ? `, ${labels.unavailable}` : ''}`}
|
|
760
766
|
accessibilityState={{ checked: selected, disabled: blocked }}
|
|
761
767
|
>
|
|
762
768
|
<Animated.View
|
|
@@ -806,10 +812,10 @@ function TimeField({
|
|
|
806
812
|
current,
|
|
807
813
|
commit,
|
|
808
814
|
reduce,
|
|
809
|
-
label
|
|
815
|
+
label: labelProp,
|
|
810
816
|
floatingLabel = true,
|
|
811
817
|
title,
|
|
812
|
-
placeholder
|
|
818
|
+
placeholder: placeholderProp,
|
|
813
819
|
size = 'md',
|
|
814
820
|
confirm,
|
|
815
821
|
error,
|
|
@@ -828,7 +834,9 @@ function TimeField({
|
|
|
828
834
|
menuStyle,
|
|
829
835
|
...chipProps
|
|
830
836
|
}: TimeFieldProps) {
|
|
831
|
-
const { colors, radius, font, dark, shadow } = useKit();
|
|
837
|
+
const { colors, radius, font, dark, shadow, labels } = useKit();
|
|
838
|
+
const label = labelProp ?? labels.time;
|
|
839
|
+
const placeholder = placeholderProp ?? labels.selectTime;
|
|
832
840
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
833
841
|
const tint = accent ?? colors.accent;
|
|
834
842
|
const bg = surface ?? colors.background;
|
|
@@ -849,19 +857,16 @@ function TimeField({
|
|
|
849
857
|
const [mounted, setMounted] = useState(false);
|
|
850
858
|
const [draft, setDraft] = useState<TimeValue | null>(current);
|
|
851
859
|
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
852
|
-
// Read by the sheet's pan responder, which is created once.
|
|
853
|
-
const reduceRef = useRef(reduce);
|
|
854
|
-
reduceRef.current = reduce;
|
|
855
860
|
|
|
856
861
|
const ok = !error && !!success;
|
|
857
862
|
const floating = open || !!current;
|
|
858
|
-
const float =
|
|
859
|
-
const focus =
|
|
860
|
-
const halo =
|
|
861
|
-
const show =
|
|
862
|
-
const drag =
|
|
863
|
-
const shake =
|
|
864
|
-
const check =
|
|
863
|
+
const [float] = useState(() => new Animated.Value(floating ? 1 : 0)); // native
|
|
864
|
+
const [focus] = useState(() => new Animated.Value(0)); // JS: colours
|
|
865
|
+
const [halo] = useState(() => new Animated.Value(0)); // native
|
|
866
|
+
const [show] = useState(() => new Animated.Value(0)); // native
|
|
867
|
+
const [drag] = useState(() => new Animated.Value(0)); // native
|
|
868
|
+
const [shake] = useState(() => new Animated.Value(0)); // native
|
|
869
|
+
const [check] = useState(() => new Animated.Value(ok ? 1 : 0)); // native
|
|
865
870
|
|
|
866
871
|
useEffect(() => {
|
|
867
872
|
if (reduce) float.setValue(floating ? 1 : 0);
|
|
@@ -918,21 +923,19 @@ function TimeField({
|
|
|
918
923
|
}
|
|
919
924
|
};
|
|
920
925
|
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
});
|
|
935
|
-
}, [drag]);
|
|
926
|
+
const back = () =>
|
|
927
|
+
reduce
|
|
928
|
+
? Animated.timing(drag, { toValue: 0, duration: 150, easing: Easing.bezier(0.32, 0.72, 0, 1), useNativeDriver: true }).start()
|
|
929
|
+
: Animated.spring(drag, { toValue: 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
930
|
+
const pan = usePanResponder({
|
|
931
|
+
onMoveShouldSetPanResponder: (_, g) => g.dy > 4 && Math.abs(g.dy) > Math.abs(g.dx),
|
|
932
|
+
onPanResponderMove: (_, g) => drag.setValue(Math.max(0, g.dy)),
|
|
933
|
+
onPanResponderRelease: (_, g) => {
|
|
934
|
+
if (g.dy > 110 || g.vy > 1.1) setOpen(false);
|
|
935
|
+
else back();
|
|
936
|
+
},
|
|
937
|
+
onPanResponderTerminate: back,
|
|
938
|
+
});
|
|
936
939
|
|
|
937
940
|
const active = error ? colors.danger : tint;
|
|
938
941
|
const rest = error ? colors.danger : ok ? colors.success : ((userBorder as string | undefined) ?? colors.input);
|
|
@@ -942,7 +945,7 @@ function TimeField({
|
|
|
942
945
|
outputRange: [error ? colors.danger : ok ? colors.success : (userLabelColor ?? colors.mutedForeground), active],
|
|
943
946
|
});
|
|
944
947
|
const topLabelColor = focus.interpolate({ inputRange: [0, 1], outputRange: [error ? colors.danger : (userLabelColor ?? colors.foreground), active] });
|
|
945
|
-
const text = current ? formatTime(current, hour12) : '';
|
|
948
|
+
const text = current ? formatTime(current, hour12, labels) : '';
|
|
946
949
|
const successText = typeof success === 'string' && ok ? success : undefined;
|
|
947
950
|
const message = error ?? successText ?? helper;
|
|
948
951
|
const tone = error ? 'error' : successText ? 'success' : 'helper';
|
|
@@ -966,7 +969,7 @@ function TimeField({
|
|
|
966
969
|
disabled={disabled}
|
|
967
970
|
accessibilityRole="button"
|
|
968
971
|
accessibilityLabel={label}
|
|
969
|
-
accessibilityValue={{ text: text ||
|
|
972
|
+
accessibilityValue={{ text: text || labels.notSet }}
|
|
970
973
|
accessibilityState={{ expanded: open, disabled: !!disabled }}
|
|
971
974
|
style={{ opacity: disabled ? 0.5 : 1 }}
|
|
972
975
|
>
|
|
@@ -1032,7 +1035,7 @@ function TimeField({
|
|
|
1032
1035
|
|
|
1033
1036
|
<Modal visible={mounted} transparent animationType="none" statusBarTranslucent onRequestClose={close}>
|
|
1034
1037
|
<Animated.View style={[StyleSheet.absoluteFill, { backgroundColor: colors.overlay, opacity: show }]}>
|
|
1035
|
-
<Pressable style={StyleSheet.absoluteFill} onPress={close} accessibilityLabel=
|
|
1038
|
+
<Pressable style={StyleSheet.absoluteFill} onPress={close} accessibilityLabel={labels.close} accessibilityRole="button" />
|
|
1036
1039
|
</Animated.View>
|
|
1037
1040
|
<View style={[styles.sheetHost, wide && styles.centered]} pointerEvents="box-none">
|
|
1038
1041
|
<Animated.View
|
|
@@ -1065,11 +1068,11 @@ function TimeField({
|
|
|
1065
1068
|
},
|
|
1066
1069
|
]}
|
|
1067
1070
|
>
|
|
1068
|
-
<View {...pan
|
|
1071
|
+
<View {...pan} style={styles.sheetHeader}>
|
|
1069
1072
|
{!wide ? <View style={[styles.grabber, { backgroundColor: colors.border }]} /> : null}
|
|
1070
1073
|
<Text style={[styles.title, ff, { color: colors.foreground }]}>{title ?? label}</Text>
|
|
1071
1074
|
{variant === 'clock' ? null : (
|
|
1072
|
-
<Text style={[styles.preview, ff, { color: draft ? tint : colors.mutedForeground }]}>{draft ? formatTime(draft, hour12) : placeholder}</Text>
|
|
1075
|
+
<Text style={[styles.preview, ff, { color: draft ? tint : colors.mutedForeground }]}>{draft ? formatTime(draft, hour12, labels) : placeholder}</Text>
|
|
1073
1076
|
)}
|
|
1074
1077
|
</View>
|
|
1075
1078
|
<ScrollView style={styles.sheetBody} contentContainerStyle={styles.sheetContent} bounces={false} scrollEnabled={variant === 'chips'}>
|
|
@@ -1084,7 +1087,7 @@ function TimeField({
|
|
|
1084
1087
|
{staged ? (
|
|
1085
1088
|
<View style={[styles.footer, { borderTopColor: colors.border }]}>
|
|
1086
1089
|
<Pressable onPress={close} accessibilityRole="button" style={({ pressed }) => [styles.footBtn, { borderRadius: radius.md, opacity: pressed ? 0.6 : 1 }]}>
|
|
1087
|
-
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, ff, { color: colors.foreground }]}>
|
|
1090
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, ff, { color: colors.foreground }]}>{labels.cancel}</Text>
|
|
1088
1091
|
</Pressable>
|
|
1089
1092
|
<Pressable
|
|
1090
1093
|
onPress={() => {
|
|
@@ -1099,7 +1102,7 @@ function TimeField({
|
|
|
1099
1102
|
{ backgroundColor: tint, borderRadius: radius.md, opacity: !draft ? 0.4 : pressed ? 0.85 : 1 },
|
|
1100
1103
|
]}
|
|
1101
1104
|
>
|
|
1102
|
-
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, ff, { color: readableOn(tint) }]}>{draft ?
|
|
1105
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, ff, { color: readableOn(tint) }]}>{draft ? labels.setTime(formatTime(draft, hour12, labels)) : labels.done}</Text>
|
|
1103
1106
|
</Pressable>
|
|
1104
1107
|
</View>
|
|
1105
1108
|
) : null}
|
|
@@ -1123,7 +1126,7 @@ function FieldMessage({
|
|
|
1123
1126
|
style?: StyleProp<TextStyle>;
|
|
1124
1127
|
}) {
|
|
1125
1128
|
const { colors, font } = useKit();
|
|
1126
|
-
const enter =
|
|
1129
|
+
const [enter] = useState(() => new Animated.Value(tone === 'helper' ? 1 : 0)); // native
|
|
1127
1130
|
useEffect(() => {
|
|
1128
1131
|
if (reduce) Animated.timing(enter, { toValue: 1, duration: 150, useNativeDriver: true }).start();
|
|
1129
1132
|
else Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
|