@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
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import * as Haptics from 'expo-haptics';
|
|
2
|
-
import React, { useEffect, useId, useRef, useState } from 'react';
|
|
3
|
-
import {
|
|
4
|
-
AccessibilityInfo,
|
|
5
|
-
ActivityIndicator,
|
|
6
|
-
Animated,
|
|
7
|
-
Pressable,
|
|
8
|
-
StyleSheet,
|
|
9
|
-
Text,
|
|
10
|
-
View,
|
|
11
|
-
type PressableProps,
|
|
12
|
-
type StyleProp,
|
|
13
|
-
type TextStyle,
|
|
14
|
-
type ViewStyle,
|
|
15
|
-
} from 'react-native';
|
|
16
|
-
import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';
|
|
17
|
-
|
|
18
|
-
import { alpha, isTextNode, motion, readableOn, useKit } from '@/lib/kit';
|
|
19
|
-
|
|
20
|
-
export type ButtonVariant =
|
|
21
|
-
| 'primary'
|
|
22
|
-
| 'accent'
|
|
23
|
-
| 'secondary'
|
|
24
|
-
| 'outline'
|
|
25
|
-
| 'ghost'
|
|
26
|
-
| 'soft'
|
|
27
|
-
| 'destructive'
|
|
28
|
-
| 'link'
|
|
29
|
-
| 'gradient'
|
|
30
|
-
| 'glow'
|
|
31
|
-
| 'elevated';
|
|
32
|
-
export type ButtonSize = 'sm' | 'md' | 'lg' | 'icon';
|
|
33
|
-
|
|
34
|
-
export interface ButtonProps extends Omit<PressableProps, 'style' | 'children'> {
|
|
35
|
-
children?: React.ReactNode;
|
|
36
|
-
variant?: ButtonVariant;
|
|
37
|
-
size?: ButtonSize;
|
|
38
|
-
/** Swaps the label for a spinner and blocks presses. */
|
|
39
|
-
loading?: boolean;
|
|
40
|
-
left?: React.ReactNode;
|
|
41
|
-
right?: React.ReactNode;
|
|
42
|
-
/** Stretch to the parent's width. */
|
|
43
|
-
block?: boolean;
|
|
44
|
-
/** Fully rounded ends. */
|
|
45
|
-
pill?: boolean;
|
|
46
|
-
/**
|
|
47
|
-
* Any colour. Recolours every tinted variant (accent, soft, outline, ghost, link, gradient,
|
|
48
|
-
* glow, elevated); text on filled buttons picks black or white automatically.
|
|
49
|
-
*/
|
|
50
|
-
color?: string;
|
|
51
|
-
/** Second gradient stop for `variant="gradient"` (default: `color` shifted along the hue wheel). */
|
|
52
|
-
gradientTo?: string;
|
|
53
|
-
/** Light tap on press (default false — most buttons shouldn't buzz). */
|
|
54
|
-
haptics?: boolean;
|
|
55
|
-
style?: StyleProp<ViewStyle>;
|
|
56
|
-
textStyle?: StyleProp<TextStyle>;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
60
|
-
// content text scales all the way.
|
|
61
|
-
const FONT_CAP = 1.35;
|
|
62
|
-
|
|
63
|
-
const SIZES: Record<ButtonSize, { h: number; px: number; font: number; gap: number }> = {
|
|
64
|
-
sm: { h: 36, px: 14, font: 14, gap: 6 },
|
|
65
|
-
md: { h: 46, px: 18, font: 15.5, gap: 8 },
|
|
66
|
-
lg: { h: 54, px: 22, font: 17, gap: 10 },
|
|
67
|
-
icon: { h: 44, px: 0, font: 15, gap: 0 },
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
/** Rotates a #rrggbb colour around the hue wheel by `deg` and nudges its lightness by `dl` (0–1). */
|
|
71
|
-
function shift(hex: string, deg: number, dl = 0) {
|
|
72
|
-
const full = hex.length === 4 ? '#' + [...hex.slice(1)].map((c) => c + c).join('') : hex;
|
|
73
|
-
const n = parseInt(full.slice(1, 7), 16);
|
|
74
|
-
if (!hex.startsWith('#') || Number.isNaN(n)) return hex;
|
|
75
|
-
const [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((v) => v / 255);
|
|
76
|
-
const max = Math.max(r, g, b);
|
|
77
|
-
const min = Math.min(r, g, b);
|
|
78
|
-
const l = (max + min) / 2;
|
|
79
|
-
const d = max - min;
|
|
80
|
-
const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
|
|
81
|
-
let h = 0;
|
|
82
|
-
if (d) h = max === r ? ((g - b) / d) % 6 : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
|
|
83
|
-
h = (h * 60 + deg + 360) % 360;
|
|
84
|
-
const L = Math.min(1, Math.max(0, l + dl));
|
|
85
|
-
const c = (1 - Math.abs(2 * L - 1)) * s;
|
|
86
|
-
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
87
|
-
const m = L - c / 2;
|
|
88
|
-
const [r1, g1, b1] = h < 60 ? [c, x, 0] : h < 120 ? [x, c, 0] : h < 180 ? [0, c, x] : h < 240 ? [0, x, c] : h < 300 ? [x, 0, c] : [c, 0, x];
|
|
89
|
-
return '#' + [r1, g1, b1].map((v) => Math.round((v + m) * 255).toString(16).padStart(2, '0')).join('');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Midpoint of two #rrggbb colours (to pick readable text for a gradient). */
|
|
93
|
-
function mid(a: string, b: string) {
|
|
94
|
-
const n = (h: string) => parseInt((h.length === 4 ? '#' + [...h.slice(1)].map((c) => c + c).join('') : h).slice(1, 7), 16);
|
|
95
|
-
const [x, y] = [n(a), n(b)];
|
|
96
|
-
if (!a.startsWith('#') || !b.startsWith('#') || Number.isNaN(x) || Number.isNaN(y)) return a;
|
|
97
|
-
return '#' + [16, 8, 0].map((sh) => Math.round((((x >> sh) & 255) + ((y >> sh) & 255)) / 2).toString(16).padStart(2, '0')).join('');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Pressable with spring feedback, 11 variants, 4 sizes, icons and a loading state. Pass any
|
|
102
|
-
* `color` and every tinted variant — including the gradient and glow looks — follows it.
|
|
103
|
-
*/
|
|
104
|
-
export function Button({
|
|
105
|
-
children,
|
|
106
|
-
variant = 'primary',
|
|
107
|
-
size = 'md',
|
|
108
|
-
loading = false,
|
|
109
|
-
left,
|
|
110
|
-
right,
|
|
111
|
-
block,
|
|
112
|
-
pill,
|
|
113
|
-
color,
|
|
114
|
-
gradientTo,
|
|
115
|
-
haptics = false,
|
|
116
|
-
disabled,
|
|
117
|
-
style,
|
|
118
|
-
textStyle,
|
|
119
|
-
onPress,
|
|
120
|
-
onPressIn,
|
|
121
|
-
onPressOut,
|
|
122
|
-
...props
|
|
123
|
-
}: ButtonProps) {
|
|
124
|
-
const { colors, radius, dark, font } = useKit();
|
|
125
|
-
const ff = font.family ? { fontFamily: font.family } : null;
|
|
126
|
-
const gid = 'btn' + useId().replace(/[^a-zA-Z0-9]/g, '');
|
|
127
|
-
const reduce = useReduceMotion();
|
|
128
|
-
const scale = useRef(new Animated.Value(1)).current;
|
|
129
|
-
const isDisabled = disabled || loading;
|
|
130
|
-
const tint = color ?? colors.accent;
|
|
131
|
-
const onTint = color ? readableOn(tint) : colors.accentForeground;
|
|
132
|
-
const s = SIZES[size];
|
|
133
|
-
const corner = pill || size === 'icon' ? radius.full : radius.md;
|
|
134
|
-
// Only the opt-in gradient variant gets a second tone; everything else is one solid colour.
|
|
135
|
-
const to = variant === 'gradient' ? gradientTo ?? shift(tint, 38, 0.04) : tint;
|
|
136
|
-
|
|
137
|
-
const look: Record<ButtonVariant, { bg: string; fg: string; border?: string }> = {
|
|
138
|
-
primary: { bg: colors.primary, fg: colors.primaryForeground },
|
|
139
|
-
accent: { bg: tint, fg: onTint },
|
|
140
|
-
secondary: { bg: colors.secondary, fg: colors.secondaryForeground },
|
|
141
|
-
outline: { bg: 'transparent', fg: color ?? colors.foreground, border: color ? alpha(tint, 0.5) : colors.border },
|
|
142
|
-
ghost: { bg: 'transparent', fg: color ?? colors.foreground },
|
|
143
|
-
soft: { bg: alpha(tint, dark ? 0.2 : 0.14), fg: tint },
|
|
144
|
-
destructive: { bg: colors.danger, fg: colors.dangerForeground },
|
|
145
|
-
link: { bg: 'transparent', fg: tint },
|
|
146
|
-
gradient: { bg: tint, fg: readableOn(mid(tint, to)) },
|
|
147
|
-
glow: { bg: tint, fg: onTint },
|
|
148
|
-
elevated: { bg: dark ? colors.popover : colors.card, fg: color ?? colors.foreground, border: alpha(colors.foreground, dark ? 0.1 : 0.06) },
|
|
149
|
-
};
|
|
150
|
-
const { bg, fg, border } = look[variant];
|
|
151
|
-
const solid = bg !== 'transparent';
|
|
152
|
-
const quiet = variant === 'ghost' || variant === 'outline' || variant === 'link';
|
|
153
|
-
|
|
154
|
-
// Reduce Motion: no squish; the pressed wash / opacity still marks the press.
|
|
155
|
-
const spring = (to: number) => {
|
|
156
|
-
if (reduce) return scale.setValue(1);
|
|
157
|
-
Animated.spring(scale, { toValue: to, useNativeDriver: true, ...motion.bouncy }).start();
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const label = isTextNode(children) ? (
|
|
161
|
-
<Text
|
|
162
|
-
numberOfLines={1}
|
|
163
|
-
maxFontSizeMultiplier={FONT_CAP}
|
|
164
|
-
style={[styles.label, ff, { color: fg, fontSize: s.font }, variant === 'link' && styles.link, textStyle]}
|
|
165
|
-
>
|
|
166
|
-
{children}
|
|
167
|
-
</Text>
|
|
168
|
-
) : (
|
|
169
|
-
children
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
return (
|
|
173
|
-
<Animated.View
|
|
174
|
-
style={[
|
|
175
|
-
{ transform: [{ scale }] },
|
|
176
|
-
block && styles.block,
|
|
177
|
-
style,
|
|
178
|
-
]}
|
|
179
|
-
>
|
|
180
|
-
<Pressable
|
|
181
|
-
accessibilityRole="button"
|
|
182
|
-
accessibilityState={{ disabled: !!isDisabled, busy: loading }}
|
|
183
|
-
disabled={isDisabled}
|
|
184
|
-
onPress={(e) => {
|
|
185
|
-
if (haptics) Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
|
|
186
|
-
onPress?.(e);
|
|
187
|
-
}}
|
|
188
|
-
onPressIn={(e) => {
|
|
189
|
-
spring(0.96);
|
|
190
|
-
onPressIn?.(e);
|
|
191
|
-
}}
|
|
192
|
-
onPressOut={(e) => {
|
|
193
|
-
spring(1);
|
|
194
|
-
onPressOut?.(e);
|
|
195
|
-
}}
|
|
196
|
-
style={({ pressed }) => [
|
|
197
|
-
styles.base,
|
|
198
|
-
{
|
|
199
|
-
minHeight: s.h,
|
|
200
|
-
height: size === 'icon' ? s.h : undefined,
|
|
201
|
-
width: size === 'icon' ? s.h : undefined,
|
|
202
|
-
paddingHorizontal: variant === 'link' ? 0 : s.px,
|
|
203
|
-
gap: s.gap,
|
|
204
|
-
borderRadius: corner,
|
|
205
|
-
backgroundColor: bg,
|
|
206
|
-
borderColor: border ?? 'transparent',
|
|
207
|
-
borderWidth: border ? StyleSheet.hairlineWidth * 2 : 0,
|
|
208
|
-
opacity: isDisabled ? 0.5 : pressed && variant === 'link' ? 0.6 : 1,
|
|
209
|
-
},
|
|
210
|
-
pressed && quiet && variant !== 'link' && { backgroundColor: color ? alpha(tint, 0.1) : colors.muted },
|
|
211
|
-
// Coloured glow under the button (iOS / web; Android shows plain elevation).
|
|
212
|
-
variant === 'glow' && !isDisabled && [styles.glow, { shadowColor: tint }],
|
|
213
|
-
variant === 'elevated' && styles.elevated,
|
|
214
|
-
]}
|
|
215
|
-
{...props}
|
|
216
|
-
>
|
|
217
|
-
{({ pressed }) => (
|
|
218
|
-
<>
|
|
219
|
-
{variant === 'gradient' || variant === 'glow' ? (
|
|
220
|
-
// Two-tone fill (gradient) or a faint white top sheen on the solid glow fill, drawn behind the content.
|
|
221
|
-
<View pointerEvents="none" style={[StyleSheet.absoluteFill, { borderRadius: corner, overflow: 'hidden' }]}>
|
|
222
|
-
<Svg width="100%" height="100%">
|
|
223
|
-
<Defs>
|
|
224
|
-
<LinearGradient id={gid} x1="0" y1="0" x2="1" y2="1">
|
|
225
|
-
{variant === 'gradient'
|
|
226
|
-
? [
|
|
227
|
-
<Stop key="a" offset="0" stopColor={tint} />,
|
|
228
|
-
<Stop key="b" offset="1" stopColor={to} />,
|
|
229
|
-
]
|
|
230
|
-
: [
|
|
231
|
-
<Stop key="a" offset="0" stopColor="#ffffff" stopOpacity={0.12} />,
|
|
232
|
-
<Stop key="b" offset="0.55" stopColor="#ffffff" stopOpacity={0} />,
|
|
233
|
-
]}
|
|
234
|
-
</LinearGradient>
|
|
235
|
-
</Defs>
|
|
236
|
-
<Rect width="100%" height="100%" fill={`url(#${gid})`} />
|
|
237
|
-
</Svg>
|
|
238
|
-
</View>
|
|
239
|
-
) : null}
|
|
240
|
-
{/* Press feedback on filled buttons: a light wash on dark fills, a shade on light ones. */}
|
|
241
|
-
{solid && pressed && !quiet ? (
|
|
242
|
-
<View
|
|
243
|
-
pointerEvents="none"
|
|
244
|
-
style={[StyleSheet.absoluteFill, { borderRadius: corner, backgroundColor: readableOn(bg) === '#ffffff' ? alpha('#ffffff', 0.14) : alpha('#000000', 0.08) }]}
|
|
245
|
-
/>
|
|
246
|
-
) : null}
|
|
247
|
-
{loading ? (
|
|
248
|
-
<View style={styles.row}>
|
|
249
|
-
<ActivityIndicator size="small" color={fg} />
|
|
250
|
-
{size !== 'icon' && isTextNode(children) ? (
|
|
251
|
-
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.label, ff, { color: fg, fontSize: s.font, opacity: 0.85 }]}>{children}</Text>
|
|
252
|
-
) : null}
|
|
253
|
-
</View>
|
|
254
|
-
) : (
|
|
255
|
-
<>
|
|
256
|
-
{left}
|
|
257
|
-
{label}
|
|
258
|
-
{right}
|
|
259
|
-
</>
|
|
260
|
-
)}
|
|
261
|
-
</>
|
|
262
|
-
)}
|
|
263
|
-
</Pressable>
|
|
264
|
-
</Animated.View>
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function useReduceMotion() {
|
|
269
|
-
const [reduce, setReduce] = useState(false);
|
|
270
|
-
useEffect(() => {
|
|
271
|
-
let alive = true;
|
|
272
|
-
AccessibilityInfo.isReduceMotionEnabled()
|
|
273
|
-
.then((r) => alive && setReduce(r))
|
|
274
|
-
.catch(() => {});
|
|
275
|
-
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
276
|
-
return () => {
|
|
277
|
-
alive = false;
|
|
278
|
-
sub.remove();
|
|
279
|
-
};
|
|
280
|
-
}, []);
|
|
281
|
-
return reduce;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const styles = StyleSheet.create({
|
|
285
|
-
base: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
|
|
286
|
-
row: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
|
287
|
-
block: { alignSelf: 'stretch' },
|
|
288
|
-
label: { fontWeight: '400', letterSpacing: -0.1 },
|
|
289
|
-
link: { textDecorationLine: 'underline' },
|
|
290
|
-
glow: {
|
|
291
|
-
shadowOpacity: 0.45,
|
|
292
|
-
shadowRadius: 14,
|
|
293
|
-
shadowOffset: { width: 0, height: 6 },
|
|
294
|
-
elevation: 6,
|
|
295
|
-
},
|
|
296
|
-
elevated: {
|
|
297
|
-
shadowColor: '#000',
|
|
298
|
-
shadowOpacity: 0.1,
|
|
299
|
-
shadowRadius: 10,
|
|
300
|
-
shadowOffset: { width: 0, height: 3 },
|
|
301
|
-
elevation: 3,
|
|
302
|
-
},
|
|
303
|
-
});
|
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
import React, { createContext, useContext, useMemo } from 'react';
|
|
2
|
-
import { Platform, useColorScheme, type TextStyle } from 'react-native';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* ExpoKit design tokens. Every component reads them through useKit(), so one
|
|
6
|
-
* <KitProvider> (optional) re-skins the whole kit: light / dark, accent colour, radius.
|
|
7
|
-
* Without a provider, components follow the system colour scheme.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
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.
|
|
13
|
-
*/
|
|
14
|
-
export const KIT_VERSION = 2;
|
|
15
|
-
|
|
16
|
-
export type ColorScheme = 'light' | 'dark';
|
|
17
|
-
|
|
18
|
-
export interface Colors {
|
|
19
|
-
background: string;
|
|
20
|
-
foreground: string;
|
|
21
|
-
/** Cards, sheets, inputs. */
|
|
22
|
-
card: string;
|
|
23
|
-
cardForeground: string;
|
|
24
|
-
/** Raised surfaces: popovers, toasts. */
|
|
25
|
-
popover: string;
|
|
26
|
-
muted: string;
|
|
27
|
-
mutedForeground: string;
|
|
28
|
-
border: string;
|
|
29
|
-
input: string;
|
|
30
|
-
primary: string;
|
|
31
|
-
primaryForeground: string;
|
|
32
|
-
secondary: string;
|
|
33
|
-
secondaryForeground: string;
|
|
34
|
-
accent: string;
|
|
35
|
-
accentForeground: string;
|
|
36
|
-
success: string;
|
|
37
|
-
warning: string;
|
|
38
|
-
danger: string;
|
|
39
|
-
dangerForeground: string;
|
|
40
|
-
ring: string;
|
|
41
|
-
overlay: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface Theme {
|
|
45
|
-
dark: boolean;
|
|
46
|
-
colors: Colors;
|
|
47
|
-
/** Corner radius scale. `radius.full` makes pills. */
|
|
48
|
-
radius: {
|
|
49
|
-
sm: number;
|
|
50
|
-
md: number;
|
|
51
|
-
lg: number;
|
|
52
|
-
xl: number;
|
|
53
|
-
full: number;
|
|
54
|
-
/** A corner designed at the default radius (12), scaled to the current one: `radius.scale(16)` is 16 at 12, 0 at 0. */
|
|
55
|
-
scale: (n: number) => number;
|
|
56
|
-
/** Corner for a bar or chip of height `h`: a pill at radius ≥ 12, softer below, square at 0. */
|
|
57
|
-
capsule: (h: number) => number;
|
|
58
|
-
};
|
|
59
|
-
/** 4pt spacing scale: space(4) === 16. */
|
|
60
|
-
space: (n: number) => number;
|
|
61
|
-
font: { family?: string; mono: string };
|
|
62
|
-
text: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl', TextStyle>;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const light: Colors = {
|
|
66
|
-
background: '#ffffff',
|
|
67
|
-
foreground: '#09090b',
|
|
68
|
-
card: '#ffffff',
|
|
69
|
-
cardForeground: '#09090b',
|
|
70
|
-
popover: '#ffffff',
|
|
71
|
-
muted: '#f4f4f5',
|
|
72
|
-
mutedForeground: '#71717a',
|
|
73
|
-
border: '#e4e4e7',
|
|
74
|
-
input: '#d4d4d8',
|
|
75
|
-
primary: '#18181b',
|
|
76
|
-
primaryForeground: '#fafafa',
|
|
77
|
-
secondary: '#f4f4f5',
|
|
78
|
-
secondaryForeground: '#18181b',
|
|
79
|
-
accent: '#18181b',
|
|
80
|
-
accentForeground: '#ffffff',
|
|
81
|
-
success: '#16a34a',
|
|
82
|
-
warning: '#f59e0b',
|
|
83
|
-
danger: '#e5484d',
|
|
84
|
-
dangerForeground: '#ffffff',
|
|
85
|
-
ring: '#18181b',
|
|
86
|
-
overlay: 'rgba(0,0,0,0.4)',
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const dark: Colors = {
|
|
90
|
-
background: '#09090b',
|
|
91
|
-
foreground: '#fafafa',
|
|
92
|
-
card: '#111113',
|
|
93
|
-
cardForeground: '#fafafa',
|
|
94
|
-
popover: '#18181b',
|
|
95
|
-
muted: '#1c1c20',
|
|
96
|
-
mutedForeground: '#a1a1aa',
|
|
97
|
-
border: '#27272a',
|
|
98
|
-
input: '#34343a',
|
|
99
|
-
primary: '#fafafa',
|
|
100
|
-
primaryForeground: '#18181b',
|
|
101
|
-
secondary: '#1f1f23',
|
|
102
|
-
secondaryForeground: '#fafafa',
|
|
103
|
-
accent: '#fafafa',
|
|
104
|
-
accentForeground: '#09090b',
|
|
105
|
-
success: '#3dd68c',
|
|
106
|
-
warning: '#ffb224',
|
|
107
|
-
danger: '#ff6369',
|
|
108
|
-
dangerForeground: '#ffffff',
|
|
109
|
-
ring: '#d4d4d8',
|
|
110
|
-
overlay: 'rgba(0,0,0,0.6)',
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
export const palettes = { light, dark };
|
|
114
|
-
|
|
115
|
-
function build(scheme: ColorScheme, overrides?: ThemeOverrides): Theme {
|
|
116
|
-
const base = scheme === 'dark' ? dark : light;
|
|
117
|
-
const accent = overrides?.accent;
|
|
118
|
-
const colors: Colors = {
|
|
119
|
-
...base,
|
|
120
|
-
...(accent ? { accent, ring: accent, accentForeground: readableOn(accent) } : null),
|
|
121
|
-
...overrides?.colors?.[scheme],
|
|
122
|
-
};
|
|
123
|
-
const r = overrides?.radius ?? 12;
|
|
124
|
-
return {
|
|
125
|
-
dark: scheme === 'dark',
|
|
126
|
-
colors,
|
|
127
|
-
radius: {
|
|
128
|
-
sm: Math.round(r * 0.5),
|
|
129
|
-
md: r,
|
|
130
|
-
lg: Math.round(r * 1.35),
|
|
131
|
-
xl: Math.round(r * 1.8),
|
|
132
|
-
full: 999,
|
|
133
|
-
scale: (n) => Math.round((n * r) / 12),
|
|
134
|
-
capsule: (h) => Math.round((h / 2) * Math.min(1, r / 12)),
|
|
135
|
-
},
|
|
136
|
-
space: (n) => n * 4,
|
|
137
|
-
font: {
|
|
138
|
-
family: overrides?.fontFamily,
|
|
139
|
-
mono: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'ui-monospace, SFMono-Regular, Menlo, monospace' })!,
|
|
140
|
-
},
|
|
141
|
-
text: {
|
|
142
|
-
xs: { fontSize: 12, lineHeight: 16 },
|
|
143
|
-
sm: { fontSize: 14, lineHeight: 20 },
|
|
144
|
-
md: { fontSize: 16, lineHeight: 22 },
|
|
145
|
-
lg: { fontSize: 18, lineHeight: 24, fontWeight: '400' },
|
|
146
|
-
xl: { fontSize: 22, lineHeight: 28, fontWeight: '400', letterSpacing: -0.3 },
|
|
147
|
-
xxl: { fontSize: 30, lineHeight: 36, fontWeight: '400', letterSpacing: -0.8 },
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export interface ThemeOverrides {
|
|
153
|
-
/** Brand colour used for focus rings, selected states and accent buttons. */
|
|
154
|
-
accent?: string;
|
|
155
|
-
/** Base corner radius in points (default 12). */
|
|
156
|
-
radius?: number;
|
|
157
|
-
fontFamily?: string;
|
|
158
|
-
/** Override any token per scheme. */
|
|
159
|
-
colors?: Partial<Record<ColorScheme, Partial<Colors>>>;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const KitContext = createContext<{ scheme?: ColorScheme; overrides?: ThemeOverrides } | null>(null);
|
|
163
|
-
|
|
164
|
-
export function KitProvider({
|
|
165
|
-
scheme,
|
|
166
|
-
children,
|
|
167
|
-
...overrides
|
|
168
|
-
}: ThemeOverrides & { scheme?: ColorScheme | 'system'; children: React.ReactNode }) {
|
|
169
|
-
const value = useMemo(
|
|
170
|
-
() => ({ scheme: scheme === 'system' ? undefined : scheme, overrides }),
|
|
171
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
172
|
-
[scheme, overrides.accent, overrides.radius, overrides.fontFamily, overrides.colors]
|
|
173
|
-
);
|
|
174
|
-
return <KitContext.Provider value={value}>{children}</KitContext.Provider>;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function useKit(): Theme {
|
|
178
|
-
const ctx = useContext(KitContext);
|
|
179
|
-
const system = useColorScheme();
|
|
180
|
-
const scheme: ColorScheme = ctx?.scheme ?? (system === 'dark' ? 'dark' : 'light');
|
|
181
|
-
return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Shared motion presets so the whole kit moves the same way. Every spring is critically damped
|
|
186
|
-
* (damping ≥ 2·√(stiffness·mass)): it settles smoothly with no overshoot or wobble.
|
|
187
|
-
*/
|
|
188
|
-
export const motion = {
|
|
189
|
-
/** Toggles, selection indicators. */
|
|
190
|
-
snappy: { damping: 30, stiffness: 260, mass: 0.8 },
|
|
191
|
-
/** Press feedback, pops. (Kept under its old name; it no longer bounces.) */
|
|
192
|
-
bouncy: { damping: 26, stiffness: 220, mass: 0.7 },
|
|
193
|
-
/** Sheets, layout changes. */
|
|
194
|
-
gentle: { damping: 27, stiffness: 160, mass: 1 },
|
|
195
|
-
fast: 160,
|
|
196
|
-
normal: 240,
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
/** Adds alpha to a #rrggbb colour: alpha('#6366f1', 0.2). */
|
|
200
|
-
export function alpha(hex: string, a: number) {
|
|
201
|
-
if (!hex.startsWith('#') || (hex.length !== 7 && hex.length !== 4)) return hex;
|
|
202
|
-
const full = hex.length === 4 ? '#' + [...hex.slice(1)].map((c) => c + c).join('') : hex;
|
|
203
|
-
return full + Math.round(Math.max(0, Math.min(1, a)) * 255).toString(16).padStart(2, '0');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** Black or white, whichever reads better on `hex`. */
|
|
207
|
-
export function readableOn(hex: string) {
|
|
208
|
-
const full = hex.length === 4 ? '#' + [...hex.slice(1)].map((c) => c + c).join('') : hex;
|
|
209
|
-
const n = parseInt(full.slice(1, 7), 16);
|
|
210
|
-
if (Number.isNaN(n)) return '#ffffff';
|
|
211
|
-
const [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
212
|
-
return (r * 299 + g * 587 + b * 114) / 1000 > 150 ? '#09090b' : '#ffffff';
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* True for children that must be wrapped in <Text>: strings, numbers and arrays of them —
|
|
217
|
-
* `{count} new` is the array [3, ' new'], and raw text outside <Text> crashes on native.
|
|
218
|
-
*/
|
|
219
|
-
export function isTextNode(node: React.ReactNode): node is string | number | (string | number)[] {
|
|
220
|
-
if (typeof node === 'string' || typeof node === 'number') return true;
|
|
221
|
-
return Array.isArray(node) && node.length > 0 && node.every((n) => typeof n === 'string' || typeof n === 'number');
|
|
222
|
-
}
|