@expokit/cli 0.4.3 → 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 +313 -4
- 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/flip-calendar.json +106 -0
- 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/paywall.json +144 -0
- 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/ruler-time-picker.json +147 -0
- 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 +462 -12
- package/registry/accordion.json +1 -0
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +50 -58
- package/registry/action-sheet/files/src/lib/kit.tsx +462 -12
- package/registry/action-sheet.json +1 -0
- package/registry/alert/files/src/components/ui/alert.tsx +20 -13
- package/registry/alert/files/src/lib/kit.tsx +462 -12
- package/registry/alert.json +3 -4
- package/registry/assistant-app.json +1 -0
- package/registry/avatar/files/src/components/ui/avatar.tsx +89 -74
- package/registry/avatar/files/src/lib/kit.tsx +462 -12
- package/registry/avatar.json +1 -0
- package/registry/badge/files/src/components/ui/badge.tsx +36 -36
- package/registry/badge/files/src/lib/kit.tsx +462 -12
- package/registry/badge.json +1 -0
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +39 -44
- package/registry/bottom-nav/files/src/lib/kit.tsx +462 -12
- package/registry/bottom-nav.json +5 -0
- package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +357 -300
- package/registry/bottom-sheet/files/src/lib/kit.tsx +462 -12
- package/registry/bottom-sheet.json +5 -1
- package/registry/bullet.json +1 -0
- package/registry/button/files/src/components/ui/button.tsx +23 -34
- package/registry/button/files/src/lib/kit.tsx +462 -12
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +6 -9
- package/registry/button-3d/files/src/lib/kit.tsx +462 -12
- package/registry/button-3d.json +1 -0
- package/registry/button.json +1 -0
- package/registry/calendar/files/src/components/ui/calendar.tsx +77 -79
- package/registry/calendar/files/src/lib/kit.tsx +462 -12
- package/registry/calendar.json +1 -0
- package/registry/card/files/src/components/ui/card.tsx +7 -6
- package/registry/card/files/src/lib/kit.tsx +462 -12
- 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 +462 -12
- package/registry/checkbox.json +1 -0
- package/registry/checkout.json +1 -0
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +24 -24
- package/registry/chip-group/files/src/lib/kit.tsx +462 -12
- 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 +47 -52
- package/registry/coupon/files/src/components/ui/flip-card.tsx +9 -14
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +80 -101
- package/registry/coupon/files/src/lib/kit.tsx +462 -12
- 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 +77 -79
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +64 -71
- package/registry/date-picker/files/src/lib/kit.tsx +462 -12
- package/registry/date-picker.json +1 -0
- package/registry/dialog/files/src/components/ui/dialog.tsx +66 -68
- package/registry/dialog/files/src/lib/kit.tsx +462 -12
- package/registry/dialog.json +1 -0
- package/registry/donut-shop.json +1 -0
- package/registry/drawer/files/src/components/ui/drawer.tsx +76 -88
- package/registry/drawer/files/src/lib/kit.tsx +462 -12
- 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 +632 -0
- package/registry/flip-calendar/files/src/lib/kit.tsx +672 -0
- package/registry/flip-calendar.json +24 -0
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +9 -14
- package/registry/flip-card/files/src/lib/kit.tsx +462 -12
- 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 +462 -12
- 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 +462 -12
- 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 +29 -16
- package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +33 -19
- 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 +43 -43
- package/registry/list-item/files/src/lib/kit.tsx +462 -12
- 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 +49 -30
- package/registry/otp-input/files/src/lib/kit.tsx +462 -12
- 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 +462 -12
- package/registry/password-input.json +1 -0
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +9 -14
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +26 -20
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +80 -101
- package/registry/payment-card/files/src/lib/kit.tsx +462 -12
- package/registry/payment-card.json +1 -0
- package/registry/paywall.json +23 -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 +31 -29
- package/registry/progress/files/src/lib/kit.tsx +462 -12
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +21 -21
- package/registry/progress-steps/files/src/lib/kit.tsx +462 -12
- 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 +462 -12
- 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 +832 -0
- package/registry/ruler-time-picker/files/src/lib/kit.tsx +672 -0
- package/registry/ruler-time-picker.json +26 -0
- package/registry/scratch-card.json +1 -0
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +28 -28
- package/registry/search-bar/files/src/lib/kit.tsx +462 -12
- 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 +11 -17
- package/registry/segmented-control/files/src/lib/kit.tsx +462 -12
- package/registry/segmented-control.json +1 -0
- package/registry/select/files/src/components/ui/select.tsx +107 -104
- package/registry/select/files/src/lib/kit.tsx +462 -12
- 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 +462 -12
- package/registry/skeleton.json +1 -0
- package/registry/slide-to-confirm.json +1 -0
- package/registry/slider/files/src/components/ui/slider.tsx +106 -108
- package/registry/slider/files/src/lib/kit.tsx +462 -12
- 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 +462 -12
- 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 +462 -12
- package/registry/stateful-button.json +5 -0
- package/registry/stepper/files/src/components/ui/stepper.tsx +40 -38
- package/registry/stepper/files/src/lib/kit.tsx +462 -12
- 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 +41 -47
- package/registry/switch/files/src/lib/kit.tsx +462 -12
- package/registry/switch.json +1 -0
- package/registry/tabs/files/src/components/ui/tabs.tsx +61 -64
- package/registry/tabs/files/src/lib/kit.tsx +462 -12
- 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 +462 -12
- 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 +462 -12
- package/registry/textarea.json +1 -0
- package/registry/ticket-card.json +1 -0
- package/registry/ticket-shape/files/src/lib/kit.tsx +462 -12
- 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 +80 -101
- package/registry/tilt-card/files/src/lib/kit.tsx +462 -12
- package/registry/tilt-card.json +1 -0
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +136 -131
- package/registry/time-picker/files/src/lib/kit.tsx +462 -12
- package/registry/time-picker.json +1 -0
- package/registry/timeline/files/src/components/ui/avatar.tsx +89 -74
- package/registry/timeline/files/src/components/ui/timeline.tsx +49 -57
- package/registry/timeline/files/src/lib/kit.tsx +462 -12
- package/registry/timeline-chart.json +1 -0
- package/registry/timeline.json +1 -0
- package/registry/toast/files/src/components/ui/toast.tsx +543 -465
- package/registry/toast/files/src/lib/kit.tsx +462 -12
- 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 -303
- package/registry/confetti/files/src/lib/kit.tsx +0 -222
- package/registry/grain/files/src/lib/kit.tsx +0 -222
- package/registry/toast/files/src/components/ui/button.tsx +0 -303
|
@@ -24,60 +24,60 @@ export interface FloatingInputProps extends Omit<TextInputProps, 'style' | 'plac
|
|
|
24
24
|
/** Sits inside the field like a placeholder, then floats onto the border when focused or filled. */
|
|
25
25
|
label: string;
|
|
26
26
|
/** `false` puts the label above the field as static text (no notch) and shows `placeholder` inside. */
|
|
27
|
-
floatingLabel?: boolean;
|
|
27
|
+
floatingLabel?: boolean | undefined;
|
|
28
28
|
/** Hint inside the empty field — only used when `floatingLabel` is false. */
|
|
29
|
-
placeholder?: string;
|
|
30
|
-
variant?: FloatingInputVariant;
|
|
29
|
+
placeholder?: string | undefined;
|
|
30
|
+
variant?: FloatingInputVariant | undefined;
|
|
31
31
|
/** Error message. Turns the field red, shows it with an icon and shakes the field once when it appears. */
|
|
32
|
-
error?: string;
|
|
32
|
+
error?: string | undefined;
|
|
33
33
|
/** Valid state: success border and an animated check. A string is shown as the message under the field. */
|
|
34
|
-
success?: boolean | string;
|
|
34
|
+
success?: boolean | string | undefined;
|
|
35
35
|
/** Hint under the field (replaced by the error / success message while one is shown). */
|
|
36
|
-
helper?: string;
|
|
36
|
+
helper?: string | undefined;
|
|
37
37
|
/** Colour of the focused border, halo and floating label (default: theme accent). */
|
|
38
|
-
accent?: string;
|
|
38
|
+
accent?: string | undefined;
|
|
39
39
|
/** Background behind the field — the floating label cuts its notch with this colour. */
|
|
40
|
-
surface?: string;
|
|
41
|
-
left?: React.ReactNode;
|
|
42
|
-
right?: React.ReactNode;
|
|
40
|
+
surface?: string | undefined;
|
|
41
|
+
left?: React.ReactNode | undefined;
|
|
42
|
+
right?: React.ReactNode | undefined;
|
|
43
43
|
/** Text before the value, shown once the label floats — '₹', '+91', 'https://'. */
|
|
44
|
-
prefix?: string;
|
|
44
|
+
prefix?: string | undefined;
|
|
45
45
|
/** Text after the value, shown once the label floats — 'INR', 'kg', '.com'. */
|
|
46
|
-
suffix?: string;
|
|
46
|
+
suffix?: string | undefined;
|
|
47
47
|
/** Show an animated clear button while there is text. */
|
|
48
|
-
clearable?: boolean;
|
|
48
|
+
clearable?: boolean | undefined;
|
|
49
49
|
/** Show `length/maxLength` under the field (needs `maxLength`). */
|
|
50
|
-
counter?: boolean;
|
|
50
|
+
counter?: boolean | undefined;
|
|
51
51
|
/** 'sm' 44pt / 14px · 'md' 54pt / 16px · 'lg' 64pt / 18px — type, padding and icons scale too. */
|
|
52
|
-
size?: 'sm' | 'md' | 'lg';
|
|
52
|
+
size?: 'sm' | 'md' | 'lg' | undefined;
|
|
53
53
|
/** Root wrapper (the one that shakes) — margins, width, flex. */
|
|
54
|
-
style?: StyleProp<ViewStyle
|
|
54
|
+
style?: StyleProp<ViewStyle> | undefined;
|
|
55
55
|
/**
|
|
56
56
|
* The bordered box: background, border width / colour, radius, height, padding. Applied after
|
|
57
57
|
* the computed style, so it wins. A `borderColor` replaces the resting border only — focus
|
|
58
58
|
* (accent), error and success colours still take over in those states. A numeric `height` /
|
|
59
59
|
* `borderRadius` also re-centres the label and resizes the focus halo.
|
|
60
60
|
*/
|
|
61
|
-
fieldStyle?: StyleProp<ViewStyle
|
|
61
|
+
fieldStyle?: StyleProp<ViewStyle> | undefined;
|
|
62
62
|
/** The typed text — fontSize, fontWeight, color, letterSpacing, fontFamily… */
|
|
63
|
-
inputStyle?: StyleProp<TextStyle
|
|
63
|
+
inputStyle?: StyleProp<TextStyle> | undefined;
|
|
64
64
|
/**
|
|
65
65
|
* The label, floating or above the field. Its `color` is the resting colour; focus (accent),
|
|
66
66
|
* error (danger) and success colours still take over in those states. With `fontSize` the
|
|
67
67
|
* floating label rests at that size and floats at the same ratio as the default, and the notch
|
|
68
68
|
* follows the rendered label width.
|
|
69
69
|
*/
|
|
70
|
-
labelStyle?: StyleProp<TextStyle
|
|
70
|
+
labelStyle?: StyleProp<TextStyle> | undefined;
|
|
71
71
|
/** Helper / error / success text under the field. `color` applies to the helper; error and success keep their tone colour. */
|
|
72
|
-
messageStyle?: StyleProp<TextStyle
|
|
72
|
+
messageStyle?: StyleProp<TextStyle> | undefined;
|
|
73
73
|
/** `prefix` text. */
|
|
74
|
-
prefixStyle?: StyleProp<TextStyle
|
|
74
|
+
prefixStyle?: StyleProp<TextStyle> | undefined;
|
|
75
75
|
/** `suffix` text. */
|
|
76
|
-
suffixStyle?: StyleProp<TextStyle
|
|
76
|
+
suffixStyle?: StyleProp<TextStyle> | undefined;
|
|
77
77
|
/** The `length/maxLength` counter. Turns danger at the limit regardless. */
|
|
78
|
-
counterStyle?: StyleProp<TextStyle
|
|
78
|
+
counterStyle?: StyleProp<TextStyle> | undefined;
|
|
79
79
|
/** Built-in icons at rest — the clear button (default: theme mutedForeground). */
|
|
80
|
-
iconColor?: string;
|
|
80
|
+
iconColor?: string | undefined;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/** Everything that scales with `size`: height, type, padding, icons and the floated label. */
|
|
@@ -169,7 +169,7 @@ export const FloatingInput = forwardRef<ComponentRef<typeof TextInput>, Floating
|
|
|
169
169
|
},
|
|
170
170
|
ref
|
|
171
171
|
) {
|
|
172
|
-
const { colors, radius, font } = useKit();
|
|
172
|
+
const { colors, radius, font, labels } = useKit();
|
|
173
173
|
const reduce = useReduceMotion();
|
|
174
174
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
175
175
|
const S = FLOATING_INPUT_SIZES[size];
|
|
@@ -197,12 +197,12 @@ export const FloatingInput = forwardRef<ComponentRef<typeof TextInput>, Floating
|
|
|
197
197
|
const ok = !error && !!success;
|
|
198
198
|
const successText = typeof success === 'string' && ok ? success : undefined;
|
|
199
199
|
|
|
200
|
-
const float =
|
|
201
|
-
const glow =
|
|
202
|
-
const focus =
|
|
203
|
-
const shake =
|
|
204
|
-
const clear =
|
|
205
|
-
const check =
|
|
200
|
+
const [float] = useState(() => new Animated.Value(floating ? 1 : 0)); // native: label + affixes
|
|
201
|
+
const [glow] = useState(() => new Animated.Value(0)); // native: halo + indicator bar
|
|
202
|
+
const [focus] = useState(() => new Animated.Value(0)); // JS: border / label colour
|
|
203
|
+
const [shake] = useState(() => new Animated.Value(0)); // native
|
|
204
|
+
const [clear] = useState(() => new Animated.Value(clearable && text ? 1 : 0)); // native
|
|
205
|
+
const [check] = useState(() => new Animated.Value(ok ? 1 : 0)); // native
|
|
206
206
|
|
|
207
207
|
useEffect(() => {
|
|
208
208
|
if (reduce) float.setValue(floating ? 1 : 0);
|
|
@@ -400,7 +400,7 @@ export const FloatingInput = forwardRef<ComponentRef<typeof TextInput>, Floating
|
|
|
400
400
|
<Pressable
|
|
401
401
|
hitSlop={(44 - S.icon) / 2}
|
|
402
402
|
accessibilityRole="button"
|
|
403
|
-
accessibilityLabel={
|
|
403
|
+
accessibilityLabel={labels.clearField(label)}
|
|
404
404
|
onPress={() => {
|
|
405
405
|
setText('');
|
|
406
406
|
inputRef.current?.focus();
|
|
@@ -493,7 +493,7 @@ function FieldMessage({
|
|
|
493
493
|
reduce: boolean;
|
|
494
494
|
}) {
|
|
495
495
|
const { font } = useKit();
|
|
496
|
-
const enter =
|
|
496
|
+
const [enter] = useState(() => new Animated.Value(tone === 'helper' ? 1 : 0)); // native
|
|
497
497
|
useEffect(() => {
|
|
498
498
|
if (tone === 'helper') return;
|
|
499
499
|
if (reduce) Animated.timing(enter, { toValue: 1, duration: 150, useNativeDriver: true }).start();
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import React, { createContext, useContext, useMemo } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import React, { createContext, useContext, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
PanResponder,
|
|
4
|
+
Platform,
|
|
5
|
+
useColorScheme,
|
|
6
|
+
useWindowDimensions,
|
|
7
|
+
type GestureResponderHandlers,
|
|
8
|
+
type PanResponderCallbacks,
|
|
9
|
+
type TextStyle,
|
|
10
|
+
type ViewStyle,
|
|
11
|
+
} from 'react-native';
|
|
3
12
|
|
|
4
13
|
/**
|
|
5
14
|
* ExpoKit design tokens. Every component reads them through useKit(), so one
|
|
@@ -8,10 +17,10 @@ import { Platform, useColorScheme, type TextStyle } from 'react-native';
|
|
|
8
17
|
*/
|
|
9
18
|
|
|
10
19
|
/**
|
|
11
|
-
* Bumped whenever components start using a new kit API (2: radius.scale / radius.capsule
|
|
12
|
-
* The CLI won't keep an edited kit.tsx older than the component being added needs.
|
|
20
|
+
* Bumped whenever components start using a new kit API (2: radius.scale / radius.capsule,
|
|
21
|
+
* 3: useLayout / shadow, 4: labels, 5: visible labels such as Cancel / Done / Today, AM / PM, usePanResponder / useLatest). The CLI won't keep an edited kit.tsx older than the component being added needs.
|
|
13
22
|
*/
|
|
14
|
-
export const KIT_VERSION =
|
|
23
|
+
export const KIT_VERSION = 5;
|
|
15
24
|
|
|
16
25
|
export type ColorScheme = 'light' | 'dark';
|
|
17
26
|
|
|
@@ -58,10 +67,300 @@ export interface Theme {
|
|
|
58
67
|
};
|
|
59
68
|
/** 4pt spacing scale: space(4) === 16. */
|
|
60
69
|
space: (n: number) => number;
|
|
61
|
-
|
|
70
|
+
/** The same soft shadow on iOS, Android and web: `style={[styles.card, shadow('md')]}`. */
|
|
71
|
+
shadow: (level: ShadowLevel, color?: string) => ViewStyle;
|
|
72
|
+
font: { family?: string | undefined; mono: string };
|
|
62
73
|
text: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl', TextStyle>;
|
|
74
|
+
/** Text the kit shows or speaks on its own (buttons, placeholders, screen-reader names). Translate with <KitProvider labels={...}>. */
|
|
75
|
+
labels: Labels;
|
|
63
76
|
}
|
|
64
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Every string the kit shows or speaks on its own: button text and placeholders (Cancel, Done, Today,
|
|
80
|
+
* Search…) and what screen readers say (close buttons, grabbers, hints). A prop on a component, like
|
|
81
|
+
* `cancelLabel` or `placeholder`, still wins over these.
|
|
82
|
+
* Pass a partial map to <KitProvider labels={...}>; keys you leave out stay English.
|
|
83
|
+
* Functions get the dynamic part, so word order can change per language.
|
|
84
|
+
*/
|
|
85
|
+
export interface Labels {
|
|
86
|
+
close: string;
|
|
87
|
+
dismiss: string;
|
|
88
|
+
closeSheet: string;
|
|
89
|
+
sheetGrabber: string;
|
|
90
|
+
sheetResizeHint: string;
|
|
91
|
+
sheetResizeOrCloseHint: string;
|
|
92
|
+
navigationDrawer: string;
|
|
93
|
+
closeDrawer: string;
|
|
94
|
+
closeMenu: string;
|
|
95
|
+
switchAccount: string;
|
|
96
|
+
openActions: string;
|
|
97
|
+
closeActions: string;
|
|
98
|
+
action: string;
|
|
99
|
+
loading: string;
|
|
100
|
+
clearSearch: string;
|
|
101
|
+
cancelSearch: string;
|
|
102
|
+
clearField: (field: string) => string;
|
|
103
|
+
back: string;
|
|
104
|
+
previous: string;
|
|
105
|
+
next: string;
|
|
106
|
+
goToToday: string;
|
|
107
|
+
goToSlide: (n: number) => string;
|
|
108
|
+
showAllCards: string;
|
|
109
|
+
expandNotification: string;
|
|
110
|
+
collapseNotification: string;
|
|
111
|
+
moreCount: (n: number) => string;
|
|
112
|
+
peopleCount: (n: number) => string;
|
|
113
|
+
namesAndMore: (names: string, n: number) => string;
|
|
114
|
+
newCount: (badge: string) => string;
|
|
115
|
+
remove: (item: string) => string;
|
|
116
|
+
selectedCount: (n: number) => string;
|
|
117
|
+
noneSelected: string;
|
|
118
|
+
notSet: string;
|
|
119
|
+
date: string;
|
|
120
|
+
time: string;
|
|
121
|
+
hour: string;
|
|
122
|
+
minute: string;
|
|
123
|
+
hours: (n: string) => string;
|
|
124
|
+
minutes: (n: number) => string;
|
|
125
|
+
booked: string;
|
|
126
|
+
unavailable: string;
|
|
127
|
+
timeDialHint: string;
|
|
128
|
+
flipCalendarHint: string;
|
|
129
|
+
increase: string;
|
|
130
|
+
decrease: string;
|
|
131
|
+
increaseItem: (item: string) => string;
|
|
132
|
+
decreaseItem: (item: string) => string;
|
|
133
|
+
range: string;
|
|
134
|
+
rangeMinimum: (name: string) => string;
|
|
135
|
+
rangeMaximum: (name: string) => string;
|
|
136
|
+
ratingStars: string;
|
|
137
|
+
ratingHearts: string;
|
|
138
|
+
showPassword: string;
|
|
139
|
+
hidePassword: string;
|
|
140
|
+
passwordStrength: (level: string) => string;
|
|
141
|
+
ruleMet: (rule: string) => string;
|
|
142
|
+
ruleUnmet: (rule: string) => string;
|
|
143
|
+
stepOf: (step: number, total: number, label: string) => string;
|
|
144
|
+
allStepsComplete: string;
|
|
145
|
+
stepCurrent: string;
|
|
146
|
+
stepCompleted: string;
|
|
147
|
+
stepNeedsAttention: string;
|
|
148
|
+
stepNotStarted: string;
|
|
149
|
+
goBackToStep: string;
|
|
150
|
+
activateCodeField: string;
|
|
151
|
+
qrCodeFor: (value: string) => string;
|
|
152
|
+
undoStroke: string;
|
|
153
|
+
signaturePad: string;
|
|
154
|
+
signaturePadSigned: string;
|
|
155
|
+
flipToBack: string;
|
|
156
|
+
flipToFront: string;
|
|
157
|
+
revealHint: string;
|
|
158
|
+
confirmHint: string;
|
|
159
|
+
prizeWheel: string;
|
|
160
|
+
spinHint: string;
|
|
161
|
+
prizes: (n: number, list: string) => string;
|
|
162
|
+
orbReveal: string;
|
|
163
|
+
orbBack: string;
|
|
164
|
+
// Visible text
|
|
165
|
+
cancel: string;
|
|
166
|
+
done: string;
|
|
167
|
+
doneCount: (n: number) => string;
|
|
168
|
+
doneDays: (n: number) => string;
|
|
169
|
+
clear: string;
|
|
170
|
+
continue: string;
|
|
171
|
+
delete: string;
|
|
172
|
+
failed: string;
|
|
173
|
+
search: string;
|
|
174
|
+
noResults: string;
|
|
175
|
+
noOptions: string;
|
|
176
|
+
nothingMatches: (query: string) => string;
|
|
177
|
+
selectPlaceholder: string;
|
|
178
|
+
selectDate: string;
|
|
179
|
+
selectDates: string;
|
|
180
|
+
pickStartDate: string;
|
|
181
|
+
pickEndDate: string;
|
|
182
|
+
selectTime: string;
|
|
183
|
+
setTime: (time: string) => string;
|
|
184
|
+
/** Clock periods, shown on 12-hour pickers and times. */
|
|
185
|
+
am: string;
|
|
186
|
+
pm: string;
|
|
187
|
+
/** Read for the AM / PM column. */
|
|
188
|
+
amPm: string;
|
|
189
|
+
morning: string;
|
|
190
|
+
afternoon: string;
|
|
191
|
+
evening: string;
|
|
192
|
+
slotsOpen: (n: number) => string;
|
|
193
|
+
full: string;
|
|
194
|
+
today: string;
|
|
195
|
+
tomorrow: string;
|
|
196
|
+
nextMonday: string;
|
|
197
|
+
inAWeek: string;
|
|
198
|
+
inAMonth: string;
|
|
199
|
+
next7Days: string;
|
|
200
|
+
nextWeekend: string;
|
|
201
|
+
thisMonth: string;
|
|
202
|
+
last7Days: string;
|
|
203
|
+
last30Days: string;
|
|
204
|
+
chooseOneOrMore: string;
|
|
205
|
+
/** After a rolling count, as in "2 selected". */
|
|
206
|
+
selectedSuffix: string;
|
|
207
|
+
chooseMonth: string;
|
|
208
|
+
chooseYear: string;
|
|
209
|
+
backToDays: string;
|
|
210
|
+
dayAndWeek: (day: number, week: number) => string;
|
|
211
|
+
daysLeft: (n: number) => string;
|
|
212
|
+
/** Too short, Weak, Okay, Strong, Very strong. */
|
|
213
|
+
passwordLevels: [string, string, string, string, string];
|
|
214
|
+
passwordsMatch: string;
|
|
215
|
+
passwordsDontMatch: string;
|
|
216
|
+
showQrCode: string;
|
|
217
|
+
copy: string;
|
|
218
|
+
copied: string;
|
|
219
|
+
slideToConfirm: string;
|
|
220
|
+
confirmed: string;
|
|
221
|
+
like: string;
|
|
222
|
+
nope: string;
|
|
223
|
+
swipeRight: string;
|
|
224
|
+
swipeLeft: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export const defaultLabels: Labels = {
|
|
228
|
+
close: 'Close',
|
|
229
|
+
dismiss: 'Dismiss',
|
|
230
|
+
closeSheet: 'Close sheet',
|
|
231
|
+
sheetGrabber: 'Sheet grabber',
|
|
232
|
+
sheetResizeHint: 'Swipe up or down to resize',
|
|
233
|
+
sheetResizeOrCloseHint: 'Swipe up or down to resize or close',
|
|
234
|
+
navigationDrawer: 'Navigation drawer',
|
|
235
|
+
closeDrawer: 'Close drawer',
|
|
236
|
+
closeMenu: 'Close menu',
|
|
237
|
+
switchAccount: 'Switch account',
|
|
238
|
+
openActions: 'Open actions',
|
|
239
|
+
closeActions: 'Close actions',
|
|
240
|
+
action: 'Action',
|
|
241
|
+
loading: 'Loading',
|
|
242
|
+
clearSearch: 'Clear search',
|
|
243
|
+
cancelSearch: 'Cancel search',
|
|
244
|
+
clearField: (field) => `Clear ${field}`,
|
|
245
|
+
back: 'Back',
|
|
246
|
+
previous: 'Previous',
|
|
247
|
+
next: 'Next',
|
|
248
|
+
goToToday: 'Go to today',
|
|
249
|
+
goToSlide: (n) => `Go to slide ${n}`,
|
|
250
|
+
showAllCards: 'Shows all cards',
|
|
251
|
+
expandNotification: 'Expands the notification',
|
|
252
|
+
collapseNotification: 'Collapses the notification',
|
|
253
|
+
moreCount: (n) => `${n} more`,
|
|
254
|
+
peopleCount: (n) => `${n} people`,
|
|
255
|
+
namesAndMore: (names, n) => `${names} and ${n} more`,
|
|
256
|
+
newCount: (badge) => `${badge} new`,
|
|
257
|
+
remove: (item) => `Remove ${item}`,
|
|
258
|
+
selectedCount: (n) => `${n} selected`,
|
|
259
|
+
noneSelected: 'None selected',
|
|
260
|
+
notSet: 'Not set',
|
|
261
|
+
date: 'Date',
|
|
262
|
+
time: 'Time',
|
|
263
|
+
hour: 'Hour',
|
|
264
|
+
minute: 'Minute',
|
|
265
|
+
hours: (n) => `${n} hours`,
|
|
266
|
+
minutes: (n) => `${n} minutes`,
|
|
267
|
+
booked: 'booked',
|
|
268
|
+
unavailable: 'unavailable',
|
|
269
|
+
timeDialHint: 'Drag around the dial, or swipe up or down to adjust.',
|
|
270
|
+
flipCalendarHint: 'Swipe up to turn to the next day, down for the previous one.',
|
|
271
|
+
increase: 'Increase',
|
|
272
|
+
decrease: 'Decrease',
|
|
273
|
+
increaseItem: (item) => `Increase ${item}`,
|
|
274
|
+
decreaseItem: (item) => `Decrease ${item}`,
|
|
275
|
+
range: 'Range',
|
|
276
|
+
rangeMinimum: (name) => `${name} minimum`,
|
|
277
|
+
rangeMaximum: (name) => `${name} maximum`,
|
|
278
|
+
ratingStars: 'Rating, stars',
|
|
279
|
+
ratingHearts: 'Rating, hearts',
|
|
280
|
+
showPassword: 'Show password',
|
|
281
|
+
hidePassword: 'Hide password',
|
|
282
|
+
passwordStrength: (level) => `Password strength: ${level}`,
|
|
283
|
+
ruleMet: (rule) => `${rule}: done`,
|
|
284
|
+
ruleUnmet: (rule) => `${rule}: not yet`,
|
|
285
|
+
stepOf: (step, total, label) => `Step ${step} of ${total}, ${label}`,
|
|
286
|
+
allStepsComplete: 'All steps complete',
|
|
287
|
+
stepCurrent: 'current',
|
|
288
|
+
stepCompleted: 'completed',
|
|
289
|
+
stepNeedsAttention: 'needs attention',
|
|
290
|
+
stepNotStarted: 'not started',
|
|
291
|
+
goBackToStep: 'Goes back to this step',
|
|
292
|
+
activateCodeField: 'Activates the code field',
|
|
293
|
+
qrCodeFor: (value) => `QR code for ${value}`,
|
|
294
|
+
undoStroke: 'Undo last stroke',
|
|
295
|
+
signaturePad: 'Signature pad, draw your signature',
|
|
296
|
+
signaturePadSigned: 'Signature pad, signed',
|
|
297
|
+
flipToBack: 'Tap to flip to the back.',
|
|
298
|
+
flipToFront: 'Showing the back. Tap to flip to the front.',
|
|
299
|
+
revealHint: 'Double-tap to reveal',
|
|
300
|
+
confirmHint: 'Double tap to confirm',
|
|
301
|
+
prizeWheel: 'Prize wheel',
|
|
302
|
+
spinHint: 'Spins the wheel for a prize',
|
|
303
|
+
prizes: (n, list) => `${n} prizes: ${list}`,
|
|
304
|
+
orbReveal: 'Reveal',
|
|
305
|
+
orbBack: 'Glass orb, drag down to go back',
|
|
306
|
+
cancel: 'Cancel',
|
|
307
|
+
done: 'Done',
|
|
308
|
+
doneCount: (n) => `Done · ${n}`,
|
|
309
|
+
doneDays: (n) => `Done · ${n} days`,
|
|
310
|
+
clear: 'Clear',
|
|
311
|
+
continue: 'Continue',
|
|
312
|
+
delete: 'Delete',
|
|
313
|
+
failed: 'Failed',
|
|
314
|
+
search: 'Search',
|
|
315
|
+
noResults: 'No results',
|
|
316
|
+
noOptions: 'No options',
|
|
317
|
+
nothingMatches: (query) => `Nothing matches “${query}”.`,
|
|
318
|
+
selectPlaceholder: 'Select…',
|
|
319
|
+
selectDate: 'Select a date',
|
|
320
|
+
selectDates: 'Select dates',
|
|
321
|
+
pickStartDate: 'Pick a start date',
|
|
322
|
+
pickEndDate: 'pick an end date',
|
|
323
|
+
selectTime: 'Select a time',
|
|
324
|
+
setTime: (time) => `Set ${time}`,
|
|
325
|
+
am: 'AM',
|
|
326
|
+
pm: 'PM',
|
|
327
|
+
amPm: 'AM or PM',
|
|
328
|
+
morning: 'Morning',
|
|
329
|
+
afternoon: 'Afternoon',
|
|
330
|
+
evening: 'Evening',
|
|
331
|
+
slotsOpen: (n) => `${n} open`,
|
|
332
|
+
full: 'Full',
|
|
333
|
+
today: 'Today',
|
|
334
|
+
tomorrow: 'Tomorrow',
|
|
335
|
+
nextMonday: 'Next Monday',
|
|
336
|
+
inAWeek: 'In a week',
|
|
337
|
+
inAMonth: 'In a month',
|
|
338
|
+
next7Days: 'Next 7 days',
|
|
339
|
+
nextWeekend: 'Next weekend',
|
|
340
|
+
thisMonth: 'This month',
|
|
341
|
+
last7Days: 'Last 7 days',
|
|
342
|
+
last30Days: 'Last 30 days',
|
|
343
|
+
chooseOneOrMore: 'Choose one or more',
|
|
344
|
+
selectedSuffix: 'selected',
|
|
345
|
+
chooseMonth: 'choose month',
|
|
346
|
+
chooseYear: 'choose year',
|
|
347
|
+
backToDays: 'back to days',
|
|
348
|
+
dayAndWeek: (day, week) => `Day ${day} · Week ${week}`,
|
|
349
|
+
daysLeft: (n) => `${n} left`,
|
|
350
|
+
passwordLevels: ['Too short', 'Weak', 'Okay', 'Strong', 'Very strong'],
|
|
351
|
+
passwordsMatch: 'Passwords match',
|
|
352
|
+
passwordsDontMatch: "Passwords don't match",
|
|
353
|
+
showQrCode: 'Show QR Code',
|
|
354
|
+
copy: 'Copy',
|
|
355
|
+
copied: 'Copied',
|
|
356
|
+
slideToConfirm: 'Slide to confirm',
|
|
357
|
+
confirmed: 'Confirmed',
|
|
358
|
+
like: 'LIKE',
|
|
359
|
+
nope: 'NOPE',
|
|
360
|
+
swipeRight: 'Right',
|
|
361
|
+
swipeLeft: 'Left',
|
|
362
|
+
};
|
|
363
|
+
|
|
65
364
|
const light: Colors = {
|
|
66
365
|
background: '#ffffff',
|
|
67
366
|
foreground: '#09090b',
|
|
@@ -134,6 +433,7 @@ function build(scheme: ColorScheme, overrides?: ThemeOverrides): Theme {
|
|
|
134
433
|
capsule: (h) => Math.round((h / 2) * Math.min(1, r / 12)),
|
|
135
434
|
},
|
|
136
435
|
space: (n) => n * 4,
|
|
436
|
+
shadow: (level, color) => shadowFor(level, scheme === 'dark', color),
|
|
137
437
|
font: {
|
|
138
438
|
family: overrides?.fontFamily,
|
|
139
439
|
mono: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'ui-monospace, SFMono-Regular, Menlo, monospace' })!,
|
|
@@ -146,20 +446,27 @@ function build(scheme: ColorScheme, overrides?: ThemeOverrides): Theme {
|
|
|
146
446
|
xl: { fontSize: 22, lineHeight: 28, fontWeight: '400', letterSpacing: -0.3 },
|
|
147
447
|
xxl: { fontSize: 30, lineHeight: 36, fontWeight: '400', letterSpacing: -0.8 },
|
|
148
448
|
},
|
|
449
|
+
labels: overrides?.labels ? { ...defaultLabels, ...overrides.labels } : defaultLabels,
|
|
149
450
|
};
|
|
150
451
|
}
|
|
151
452
|
|
|
152
453
|
export interface ThemeOverrides {
|
|
153
454
|
/** Brand colour used for focus rings, selected states and accent buttons. */
|
|
154
|
-
accent?: string;
|
|
455
|
+
accent?: string | undefined;
|
|
456
|
+
|
|
155
457
|
/** Base corner radius in points (default 12). */
|
|
156
|
-
radius?: number;
|
|
157
|
-
|
|
458
|
+
radius?: number | undefined;
|
|
459
|
+
|
|
460
|
+
fontFamily?: string | undefined;
|
|
461
|
+
|
|
158
462
|
/** Override any token per scheme. */
|
|
159
|
-
colors?: Partial<Record<ColorScheme, Partial<Colors
|
|
463
|
+
colors?: Partial<Record<ColorScheme, Partial<Colors>>> | undefined;
|
|
464
|
+
|
|
465
|
+
/** Screen-reader strings in your language, e.g. { close: "बंद करें" }. Keys you leave out stay English. */
|
|
466
|
+
labels?: Partial<Labels> | undefined;
|
|
160
467
|
}
|
|
161
468
|
|
|
162
|
-
const KitContext = createContext<{ scheme?: ColorScheme; overrides?: ThemeOverrides } | null>(null);
|
|
469
|
+
const KitContext = createContext<{ scheme?: ColorScheme | undefined; overrides?: ThemeOverrides | undefined } | null>(null);
|
|
163
470
|
|
|
164
471
|
export function KitProvider({
|
|
165
472
|
scheme,
|
|
@@ -169,7 +476,7 @@ export function KitProvider({
|
|
|
169
476
|
const value = useMemo(
|
|
170
477
|
() => ({ scheme: scheme === 'system' ? undefined : scheme, overrides }),
|
|
171
478
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
172
|
-
[scheme, overrides.accent, overrides.radius, overrides.fontFamily, overrides.colors]
|
|
479
|
+
[scheme, overrides.accent, overrides.radius, overrides.fontFamily, overrides.colors, overrides.labels]
|
|
173
480
|
);
|
|
174
481
|
return <KitContext.Provider value={value}>{children}</KitContext.Provider>;
|
|
175
482
|
}
|
|
@@ -181,6 +488,101 @@ export function useKit(): Theme {
|
|
|
181
488
|
return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
|
|
182
489
|
}
|
|
183
490
|
|
|
491
|
+
export type ShadowLevel = 'none' | 'sm' | 'md' | 'lg' | 'xl';
|
|
492
|
+
|
|
493
|
+
const SHADOWS: Record<Exclude<ShadowLevel, 'none'>, [y: number, blur: number, opacity: number]> = {
|
|
494
|
+
sm: [1, 3, 0.08],
|
|
495
|
+
md: [4, 14, 0.1],
|
|
496
|
+
lg: [10, 30, 0.14],
|
|
497
|
+
xl: [18, 48, 0.2],
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* A shadow as `boxShadow`, which React Native (0.76+) draws identically on iOS, Android and web —
|
|
502
|
+
* unlike `elevation`, whose Android shadow looks nothing like the iOS one. Darker in dark mode so
|
|
503
|
+
* it still reads on near-black. `color` is a #rrggbb tint (default black).
|
|
504
|
+
*/
|
|
505
|
+
export function shadowFor(level: ShadowLevel, dark: boolean, color = '#000000'): ViewStyle {
|
|
506
|
+
if (level === 'none') return {};
|
|
507
|
+
const [y, blur, opacity] = SHADOWS[level];
|
|
508
|
+
const a = Math.min(0.6, dark ? opacity * 2.8 : opacity);
|
|
509
|
+
const full = color.length === 4 ? '#' + [...color.slice(1)].map((c) => c + c).join('') : color;
|
|
510
|
+
const n = parseInt(full.slice(1, 7), 16);
|
|
511
|
+
const [r, g, b] = Number.isNaN(n) ? [0, 0, 0] : [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
512
|
+
return { boxShadow: `0px ${y}px ${blur}px rgba(${r},${g},${b},${+a.toFixed(3)})` };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Screen size classes, shared by every component so phones, foldables and tablets all get the
|
|
517
|
+
* right layout without any work from you. `compact` — phones (any orientation); `medium` — open
|
|
518
|
+
* foldables (Galaxy Z Fold, Pixel Fold), Surface Duo and small tablets; `expanded` — iPad and
|
|
519
|
+
* larger. Decided by the screen's short side, so a phone on its side stays a phone.
|
|
520
|
+
*/
|
|
521
|
+
export type SizeClass = 'compact' | 'medium' | 'expanded';
|
|
522
|
+
|
|
523
|
+
export const breakpoints = { medium: 600, expanded: 840 };
|
|
524
|
+
|
|
525
|
+
/** Widest a sheet / picker / popover card and a screen's content column get on big screens. */
|
|
526
|
+
export const layoutLimits = { sheet: 560, content: 720, gutter: 24 };
|
|
527
|
+
|
|
528
|
+
export function sizeClassOf(shortSide: number): SizeClass {
|
|
529
|
+
if (!(shortSide >= breakpoints.medium)) return 'compact';
|
|
530
|
+
return shortSide >= breakpoints.expanded ? 'expanded' : 'medium';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export interface Layout {
|
|
534
|
+
width: number;
|
|
535
|
+
height: number;
|
|
536
|
+
size: SizeClass;
|
|
537
|
+
/** Foldable, tablet or desktop — anything bigger than a phone. */
|
|
538
|
+
wide: boolean;
|
|
539
|
+
landscape: boolean;
|
|
540
|
+
fontScale: number;
|
|
541
|
+
/** Width for a bottom sheet, picker or action sheet: the full screen on a portrait phone, a centred card on anything wider. */
|
|
542
|
+
sheetWidth: number;
|
|
543
|
+
/** Width for a screen's main column (forms, feeds, blocks): full on phones, a readable centred column on tablets. */
|
|
544
|
+
contentWidth: number;
|
|
545
|
+
/** Grid columns that fit the current width: 1 phone, 2 foldable / landscape phone, 3 tablet. */
|
|
546
|
+
columns: number;
|
|
547
|
+
/** A value per size class; a missing one falls back to the next smaller: `pick({ compact: 16, expanded: 24 })`. */
|
|
548
|
+
pick: <T>(values: { compact: T; medium?: T; expanded?: T }) => T;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const finite = (n: number) => (Number.isFinite(n) && n > 0 ? n : 0);
|
|
552
|
+
|
|
553
|
+
/** The layout for a window of `width` × `height` points (the pure part of useLayout). */
|
|
554
|
+
export function layoutFor(rawWidth: number, rawHeight: number, fontScale = 1): Layout {
|
|
555
|
+
const width = finite(rawWidth);
|
|
556
|
+
const height = finite(rawHeight);
|
|
557
|
+
const size = sizeClassOf(Math.min(width, height || width));
|
|
558
|
+
const roomy = width >= breakpoints.medium;
|
|
559
|
+
const { sheet, content, gutter } = layoutLimits;
|
|
560
|
+
return {
|
|
561
|
+
width,
|
|
562
|
+
height,
|
|
563
|
+
size,
|
|
564
|
+
wide: size !== 'compact',
|
|
565
|
+
landscape: width > height,
|
|
566
|
+
fontScale,
|
|
567
|
+
sheetWidth: roomy ? Math.min(sheet, width - gutter * 2) : width,
|
|
568
|
+
contentWidth: roomy ? Math.min(content, width - gutter * 2) : width,
|
|
569
|
+
columns: width >= breakpoints.expanded + 160 ? 3 : roomy ? 2 : 1,
|
|
570
|
+
pick: (values) =>
|
|
571
|
+
(size === 'expanded' ? values.expanded ?? values.medium : size === 'medium' ? values.medium : undefined) ??
|
|
572
|
+
values.compact,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* The current screen's layout; re-renders on rotation, split view, Stage Manager and fold / unfold.
|
|
578
|
+
* Components use it so they look right on every device; use it in your own screens the same way:
|
|
579
|
+
* `const { contentWidth } = useLayout()` → `<View style={{ width: '100%', maxWidth: contentWidth, alignSelf: 'center' }}>`.
|
|
580
|
+
*/
|
|
581
|
+
export function useLayout(): Layout {
|
|
582
|
+
const { width, height, fontScale } = useWindowDimensions();
|
|
583
|
+
return useMemo(() => layoutFor(width, height, fontScale), [width, height, fontScale]);
|
|
584
|
+
}
|
|
585
|
+
|
|
184
586
|
/**
|
|
185
587
|
* Shared motion presets so the whole kit moves the same way. Every spring is critically damped
|
|
186
588
|
* (damping ≥ 2·√(stiffness·mass)): it settles smoothly with no overshoot or wobble.
|
|
@@ -220,3 +622,51 @@ export function isTextNode(node: React.ReactNode): node is string | number | (st
|
|
|
220
622
|
if (typeof node === 'string' || typeof node === 'number') return true;
|
|
221
623
|
return Array.isArray(node) && node.length > 0 && node.every((n) => typeof n === 'string' || typeof n === 'number');
|
|
222
624
|
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* A ref that always holds the latest `value`, for gesture and timer callbacks that must not go stale.
|
|
628
|
+
* Read `.current` only inside callbacks and effects, never while rendering.
|
|
629
|
+
*/
|
|
630
|
+
export function useLatest<T>(value: T): React.MutableRefObject<T> {
|
|
631
|
+
const ref = useRef(value);
|
|
632
|
+
useLayoutEffect(() => {
|
|
633
|
+
ref.current = value;
|
|
634
|
+
});
|
|
635
|
+
return ref;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const panKeys = [
|
|
639
|
+
'onStartShouldSetPanResponder',
|
|
640
|
+
'onStartShouldSetPanResponderCapture',
|
|
641
|
+
'onMoveShouldSetPanResponder',
|
|
642
|
+
'onMoveShouldSetPanResponderCapture',
|
|
643
|
+
'onPanResponderGrant',
|
|
644
|
+
'onPanResponderStart',
|
|
645
|
+
'onPanResponderMove',
|
|
646
|
+
'onPanResponderEnd',
|
|
647
|
+
'onPanResponderRelease',
|
|
648
|
+
'onPanResponderReject',
|
|
649
|
+
'onPanResponderTerminate',
|
|
650
|
+
] as const;
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* PanResponder handlers that always call the latest `config`, created once. Write the callbacks inline:
|
|
654
|
+
* they may read refs and props freely, and nothing is recreated between renders, so a drag in progress
|
|
655
|
+
* is never cut off. Spread the result: `<View {...pan} />`.
|
|
656
|
+
*/
|
|
657
|
+
export function usePanResponder(config: PanResponderCallbacks): GestureResponderHandlers {
|
|
658
|
+
const latest = useLatest(config);
|
|
659
|
+
const [handlers] = useState(() => {
|
|
660
|
+
const forward: Record<string, (...args: never[]) => unknown> = {};
|
|
661
|
+
for (const key of panKeys) {
|
|
662
|
+
forward[key] = (...args) => (latest.current[key] as ((...a: never[]) => unknown) | undefined)?.(...args);
|
|
663
|
+
}
|
|
664
|
+
// React Native's defaults when these are left out are `true`, so keep them when the config has none.
|
|
665
|
+
forward.onPanResponderTerminationRequest = (...args) =>
|
|
666
|
+
(latest.current.onPanResponderTerminationRequest as ((...a: never[]) => boolean) | undefined)?.(...args) ?? true;
|
|
667
|
+
forward.onShouldBlockNativeResponder = (...args) =>
|
|
668
|
+
(latest.current.onShouldBlockNativeResponder as ((...a: never[]) => boolean) | undefined)?.(...args) ?? true;
|
|
669
|
+
return PanResponder.create(forward as PanResponderCallbacks).panHandlers;
|
|
670
|
+
});
|
|
671
|
+
return handlers;
|
|
672
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"title": "Floating Input",
|
|
5
5
|
"description": "The label starts as the placeholder and glides onto the border when you focus — a soft accent halo blooms around the field, errors shake in with an icon, valid values pop a spring check. Prefix/suffix, clear button, counter and three variants whose indicator grows from the centre.",
|
|
6
6
|
"tier": "free",
|
|
7
|
+
"aliases": [],
|
|
7
8
|
"dependencies": [
|
|
8
9
|
"react-native-svg"
|
|
9
10
|
],
|
package/registry/food-app.json
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"title": "Food ordering app",
|
|
5
5
|
"description": "A complete Expo Router app for a donut shop: onboarding once, sign in, a rolling menu, coupons, bag with checkout, profile and settings with a working light / dark switch.",
|
|
6
6
|
"tier": "pro",
|
|
7
|
+
"aliases": [],
|
|
7
8
|
"dependencies": [
|
|
8
9
|
"react-native-svg",
|
|
9
10
|
"expo-haptics",
|