@expokit/cli 0.0.0-stage → 0.3.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/LICENSE +21 -0
- package/README.md +46 -2
- package/bin/expo-kit.js +1266 -0
- package/bin/lib.mjs +343 -0
- package/bin/mcp.mjs +345 -0
- package/package.json +49 -4
- package/registry/_docs/accordion.json +115 -0
- package/registry/_docs/action-sheet.json +138 -0
- package/registry/_docs/alert.json +155 -0
- package/registry/_docs/avatar.json +227 -0
- package/registry/_docs/badge.json +148 -0
- package/registry/_docs/bottom-nav.json +143 -0
- package/registry/_docs/bottom-sheet.json +186 -0
- package/registry/_docs/bullet.json +27 -0
- package/registry/_docs/button-3d.json +138 -0
- package/registry/_docs/button.json +131 -0
- package/registry/_docs/calendar.json +198 -0
- package/registry/_docs/card-stack.json +124 -0
- package/registry/_docs/card.json +131 -0
- package/registry/_docs/carousel.json +122 -0
- package/registry/_docs/chat.json +184 -0
- package/registry/_docs/checkbox.json +109 -0
- package/registry/_docs/checkout.json +105 -0
- package/registry/_docs/chip-group.json +132 -0
- package/registry/_docs/color-swatch.json +82 -0
- package/registry/_docs/confetti.json +102 -0
- package/registry/_docs/coupon.json +192 -0
- package/registry/_docs/cube-pager.json +100 -0
- package/registry/_docs/dashboard.json +123 -0
- package/registry/_docs/date-picker.json +269 -0
- package/registry/_docs/dialog.json +186 -0
- package/registry/_docs/drawer.json +199 -0
- package/registry/_docs/fab.json +144 -0
- package/registry/_docs/flip-card.json +127 -0
- package/registry/_docs/floating-input.json +211 -0
- package/registry/_docs/gift-card.json +124 -0
- package/registry/_docs/glass-button.json +142 -0
- package/registry/_docs/glass-nav-bar.json +102 -0
- package/registry/_docs/glass-search-bar.json +114 -0
- package/registry/_docs/glass-segmented.json +94 -0
- package/registry/_docs/glass-sheet.json +136 -0
- package/registry/_docs/glass-tab-bar.json +100 -0
- package/registry/_docs/glass.json +94 -0
- package/registry/_docs/inbox.json +75 -0
- package/registry/_docs/isometric-bar.json +75 -0
- package/registry/_docs/kit.json +14 -0
- package/registry/_docs/kpi-card.json +14 -0
- package/registry/_docs/like-button.json +92 -0
- package/registry/_docs/liquidglass-tab.json +30 -0
- package/registry/_docs/list-item.json +216 -0
- package/registry/_docs/loyalty-card.json +124 -0
- package/registry/_docs/morph-button.json +128 -0
- package/registry/_docs/number-ticker.json +112 -0
- package/registry/_docs/offers.json +81 -0
- package/registry/_docs/onboarding.json +69 -0
- package/registry/_docs/otp-input.json +158 -0
- package/registry/_docs/pagination-dots.json +108 -0
- package/registry/_docs/parallax-layers.json +104 -0
- package/registry/_docs/parallax-scroll.json +210 -0
- package/registry/_docs/password-input.json +112 -0
- package/registry/_docs/payment-card.json +169 -0
- package/registry/_docs/pixel-bar.json +69 -0
- package/registry/_docs/polar-area.json +81 -0
- package/registry/_docs/profile.json +87 -0
- package/registry/_docs/progress-steps.json +116 -0
- package/registry/_docs/progress.json +152 -0
- package/registry/_docs/radio-group.json +112 -0
- package/registry/_docs/rating.json +114 -0
- package/registry/_docs/ring-carousel.json +122 -0
- package/registry/_docs/scratch-card.json +110 -0
- package/registry/_docs/search-bar.json +160 -0
- package/registry/_docs/search-results.json +123 -0
- package/registry/_docs/segmented-control.json +123 -0
- package/registry/_docs/select.json +277 -0
- package/registry/_docs/settings.json +75 -0
- package/registry/_docs/shimmer-button.json +98 -0
- package/registry/_docs/sign-in.json +201 -0
- package/registry/_docs/skeleton.json +162 -0
- package/registry/_docs/slide-to-confirm.json +108 -0
- package/registry/_docs/slider.json +177 -0
- package/registry/_docs/spin-wheel.json +138 -0
- package/registry/_docs/spinner.json +121 -0
- package/registry/_docs/stateful-button.json +184 -0
- package/registry/_docs/stepper.json +184 -0
- package/registry/_docs/swipe-deck.json +118 -0
- package/registry/_docs/swipeable-row.json +95 -0
- package/registry/_docs/switch.json +124 -0
- package/registry/_docs/tabs.json +129 -0
- package/registry/_docs/tag-input.json +148 -0
- package/registry/_docs/text-animation.json +141 -0
- package/registry/_docs/textarea.json +191 -0
- package/registry/_docs/ticket-card.json +194 -0
- package/registry/_docs/ticket-shape.json +14 -0
- package/registry/_docs/tilt-card.json +157 -0
- package/registry/_docs/time-picker.json +269 -0
- package/registry/_docs/timeline-chart.json +33 -0
- package/registry/_docs/timeline.json +172 -0
- package/registry/_docs/toast.json +185 -0
- package/registry/_docs/treemap.json +33 -0
- package/registry/_docs/waterfall.json +39 -0
- package/registry/_docs/wheel-list.json +122 -0
- package/registry/accordion/files/src/components/ui/accordion.tsx +233 -0
- package/registry/accordion/files/src/lib/kit.tsx +219 -0
- package/registry/accordion.json +17 -0
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +580 -0
- package/registry/action-sheet/files/src/lib/kit.tsx +219 -0
- package/registry/action-sheet.json +18 -0
- package/registry/alert/files/src/components/ui/alert.tsx +654 -0
- package/registry/alert/files/src/components/ui/button.tsx +274 -0
- package/registry/alert/files/src/lib/kit.tsx +219 -0
- package/registry/alert.json +20 -0
- package/registry/avatar/files/src/components/ui/avatar.tsx +790 -0
- package/registry/avatar/files/src/lib/kit.tsx +219 -0
- package/registry/avatar.json +18 -0
- package/registry/badge/files/src/components/ui/badge.tsx +626 -0
- package/registry/badge/files/src/lib/kit.tsx +219 -0
- package/registry/badge.json +17 -0
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +1050 -0
- package/registry/bottom-nav/files/src/lib/kit.tsx +219 -0
- package/registry/bottom-nav.json +18 -0
- package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +585 -0
- package/registry/bottom-sheet/files/src/lib/kit.tsx +219 -0
- package/registry/bottom-sheet.json +17 -0
- package/registry/bullet.json +14 -0
- package/registry/button/files/src/components/ui/button.tsx +274 -0
- package/registry/button/files/src/lib/kit.tsx +219 -0
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +212 -0
- package/registry/button-3d/files/src/lib/kit.tsx +219 -0
- package/registry/button-3d.json +18 -0
- package/registry/button.json +18 -0
- package/registry/calendar/files/src/components/ui/calendar.tsx +822 -0
- package/registry/calendar/files/src/lib/kit.tsx +219 -0
- package/registry/calendar.json +17 -0
- package/registry/card/files/src/components/ui/card.tsx +300 -0
- package/registry/card/files/src/lib/kit.tsx +219 -0
- package/registry/card-stack.json +15 -0
- package/registry/card.json +17 -0
- package/registry/carousel.json +15 -0
- package/registry/chat.json +18 -0
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +150 -0
- package/registry/checkbox/files/src/lib/kit.tsx +219 -0
- package/registry/checkbox.json +17 -0
- package/registry/checkout.json +26 -0
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +269 -0
- package/registry/chip-group/files/src/lib/kit.tsx +219 -0
- package/registry/chip-group.json +17 -0
- package/registry/color-swatch.json +17 -0
- package/registry/confetti/files/src/components/ui/confetti.tsx +222 -0
- package/registry/confetti/files/src/lib/kit.tsx +219 -0
- package/registry/confetti.json +15 -0
- package/registry/coupon/files/src/components/ui/coupon.tsx +698 -0
- package/registry/coupon/files/src/components/ui/flip-card.tsx +205 -0
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +635 -0
- package/registry/coupon/files/src/lib/kit.tsx +219 -0
- package/registry/coupon/files/src/lib/ticket-shape.tsx +543 -0
- package/registry/coupon.json +23 -0
- package/registry/cube-pager.json +15 -0
- package/registry/dashboard.json +32 -0
- package/registry/date-picker/files/src/components/ui/calendar.tsx +822 -0
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +796 -0
- package/registry/date-picker/files/src/lib/kit.tsx +219 -0
- package/registry/date-picker.json +19 -0
- package/registry/dialog/files/src/components/ui/dialog.tsx +759 -0
- package/registry/dialog/files/src/lib/kit.tsx +219 -0
- package/registry/dialog.json +18 -0
- package/registry/drawer/files/src/components/ui/drawer.tsx +1135 -0
- package/registry/drawer/files/src/lib/kit.tsx +219 -0
- package/registry/drawer.json +18 -0
- package/registry/fab.json +18 -0
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +205 -0
- package/registry/flip-card/files/src/lib/kit.tsx +219 -0
- package/registry/flip-card.json +15 -0
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +528 -0
- package/registry/floating-input/files/src/lib/kit.tsx +219 -0
- package/registry/floating-input.json +17 -0
- package/registry/gift-card.json +21 -0
- package/registry/glass-button.json +22 -0
- package/registry/glass-nav-bar.json +21 -0
- package/registry/glass-search-bar.json +21 -0
- package/registry/glass-segmented.json +22 -0
- package/registry/glass-sheet.json +22 -0
- package/registry/glass-tab-bar.json +22 -0
- package/registry/glass.json +19 -0
- package/registry/inbox.json +30 -0
- package/registry/isometric-bar.json +14 -0
- package/registry/kit/files/src/lib/kit.tsx +219 -0
- package/registry/kit.json +11 -0
- package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +45 -0
- package/registry/kpi-card.json +14 -0
- package/registry/like-button.json +18 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +97 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +5 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/explore.tsx +5 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/index.tsx +5 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/profile.tsx +5 -0
- package/registry/liquidglass-tab/files/src/app/_layout.tsx +33 -0
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +259 -0
- package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +122 -0
- package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +61 -0
- package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +130 -0
- package/registry/liquidglass-tab/files/src/lib/tabs.ts +108 -0
- package/registry/liquidglass-tab.json +23 -0
- package/registry/list-item/files/src/components/ui/list-item.tsx +508 -0
- package/registry/list-item/files/src/lib/kit.tsx +219 -0
- package/registry/list-item.json +17 -0
- package/registry/loyalty-card.json +23 -0
- package/registry/morph-button.json +18 -0
- package/registry/number-ticker.json +15 -0
- package/registry/offers.json +24 -0
- package/registry/onboarding.json +17 -0
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +380 -0
- package/registry/otp-input/files/src/lib/kit.tsx +219 -0
- package/registry/otp-input.json +17 -0
- package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +262 -0
- package/registry/pagination-dots/files/src/lib/kit.tsx +219 -0
- package/registry/pagination-dots.json +15 -0
- package/registry/parallax-layers.json +15 -0
- package/registry/parallax-scroll.json +17 -0
- package/registry/password-input/files/src/components/ui/floating-input.tsx +528 -0
- package/registry/password-input/files/src/components/ui/password-input.tsx +271 -0
- package/registry/password-input/files/src/lib/kit.tsx +219 -0
- package/registry/password-input.json +19 -0
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +205 -0
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +385 -0
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +635 -0
- package/registry/payment-card/files/src/lib/kit.tsx +219 -0
- package/registry/payment-card.json +21 -0
- package/registry/pixel-bar.json +14 -0
- package/registry/polar-area.json +14 -0
- package/registry/profile.json +17 -0
- package/registry/progress/files/src/components/ui/progress.tsx +883 -0
- package/registry/progress/files/src/lib/kit.tsx +219 -0
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +559 -0
- package/registry/progress-steps/files/src/lib/kit.tsx +219 -0
- package/registry/progress-steps.json +18 -0
- package/registry/progress.json +17 -0
- package/registry/radio-group/files/src/components/ui/radio-group.tsx +327 -0
- package/registry/radio-group/files/src/lib/kit.tsx +219 -0
- package/registry/radio-group.json +18 -0
- package/registry/rating.json +18 -0
- package/registry/ring-carousel.json +15 -0
- package/registry/scratch-card.json +18 -0
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +530 -0
- package/registry/search-bar/files/src/lib/kit.tsx +219 -0
- package/registry/search-bar.json +17 -0
- package/registry/search-results.json +30 -0
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +341 -0
- package/registry/segmented-control/files/src/lib/kit.tsx +219 -0
- package/registry/segmented-control.json +18 -0
- package/registry/select/files/src/components/ui/select.tsx +1398 -0
- package/registry/select/files/src/lib/kit.tsx +219 -0
- package/registry/select.json +17 -0
- package/registry/settings.json +26 -0
- package/registry/shimmer-button.json +17 -0
- package/registry/sign-in.json +26 -0
- package/registry/skeleton/files/src/components/ui/skeleton.tsx +799 -0
- package/registry/skeleton/files/src/lib/kit.tsx +219 -0
- package/registry/skeleton.json +17 -0
- package/registry/slide-to-confirm.json +18 -0
- package/registry/slider/files/src/components/ui/slider.tsx +716 -0
- package/registry/slider/files/src/lib/kit.tsx +219 -0
- package/registry/slider.json +17 -0
- package/registry/spin-wheel.json +18 -0
- package/registry/spinner/files/src/components/ui/spinner.tsx +748 -0
- package/registry/spinner/files/src/lib/kit.tsx +219 -0
- package/registry/spinner.json +17 -0
- package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +532 -0
- package/registry/stateful-button/files/src/lib/kit.tsx +219 -0
- package/registry/stateful-button.json +18 -0
- package/registry/stepper/files/src/components/ui/stepper.tsx +646 -0
- package/registry/stepper/files/src/lib/kit.tsx +219 -0
- package/registry/stepper.json +17 -0
- package/registry/swipe-deck.json +15 -0
- package/registry/swipeable-row.json +15 -0
- package/registry/switch/files/src/components/ui/switch.tsx +344 -0
- package/registry/switch/files/src/lib/kit.tsx +219 -0
- package/registry/switch.json +18 -0
- package/registry/tabs/files/src/components/ui/tabs.tsx +539 -0
- package/registry/tabs/files/src/lib/kit.tsx +219 -0
- package/registry/tabs.json +17 -0
- package/registry/tag-input/files/src/components/ui/tag-input.tsx +720 -0
- package/registry/tag-input/files/src/lib/kit.tsx +219 -0
- package/registry/tag-input.json +17 -0
- package/registry/text-animation/files/src/components/ui/text-animation.tsx +650 -0
- package/registry/text-animation/files/src/lib/kit.tsx +219 -0
- package/registry/text-animation.json +15 -0
- package/registry/textarea/files/src/components/ui/textarea.tsx +553 -0
- package/registry/textarea/files/src/lib/kit.tsx +219 -0
- package/registry/textarea.json +17 -0
- package/registry/ticket-card.json +24 -0
- package/registry/ticket-shape/files/src/lib/kit.tsx +219 -0
- package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +543 -0
- package/registry/ticket-shape.json +17 -0
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +635 -0
- package/registry/tilt-card/files/src/lib/kit.tsx +219 -0
- package/registry/tilt-card.json +17 -0
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +1159 -0
- package/registry/time-picker/files/src/lib/kit.tsx +219 -0
- package/registry/time-picker.json +17 -0
- package/registry/timeline/files/src/components/ui/avatar.tsx +790 -0
- package/registry/timeline/files/src/components/ui/timeline.tsx +1059 -0
- package/registry/timeline/files/src/lib/kit.tsx +219 -0
- package/registry/timeline-chart.json +14 -0
- package/registry/timeline.json +20 -0
- package/registry/toast/files/src/components/ui/button.tsx +274 -0
- package/registry/toast/files/src/components/ui/toast.tsx +1284 -0
- package/registry/toast/files/src/lib/kit.tsx +219 -0
- package/registry/toast.json +20 -0
- package/registry/treemap.json +14 -0
- package/registry/waterfall.json +14 -0
- package/registry/wheel-list.json +17 -0
- package/schema/components.schema.json +13 -0
|
@@ -0,0 +1,219 @@
|
|
|
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
|
+
/** Shared motion presets so the whole kit moves the same way. */
|
|
185
|
+
export const motion = {
|
|
186
|
+
/** Toggles, selection indicators. */
|
|
187
|
+
snappy: { damping: 18, stiffness: 260, mass: 0.8 },
|
|
188
|
+
/** Press feedback, pops. */
|
|
189
|
+
bouncy: { damping: 11, stiffness: 220, mass: 0.7 },
|
|
190
|
+
/** Sheets, layout changes. */
|
|
191
|
+
gentle: { damping: 22, stiffness: 160, mass: 1 },
|
|
192
|
+
fast: 160,
|
|
193
|
+
normal: 240,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** Adds alpha to a #rrggbb colour: alpha('#6366f1', 0.2). */
|
|
197
|
+
export function alpha(hex: string, a: number) {
|
|
198
|
+
if (!hex.startsWith('#') || (hex.length !== 7 && hex.length !== 4)) return hex;
|
|
199
|
+
const full = hex.length === 4 ? '#' + [...hex.slice(1)].map((c) => c + c).join('') : hex;
|
|
200
|
+
return full + Math.round(Math.max(0, Math.min(1, a)) * 255).toString(16).padStart(2, '0');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Black or white, whichever reads better on `hex`. */
|
|
204
|
+
export function readableOn(hex: string) {
|
|
205
|
+
const full = hex.length === 4 ? '#' + [...hex.slice(1)].map((c) => c + c).join('') : hex;
|
|
206
|
+
const n = parseInt(full.slice(1, 7), 16);
|
|
207
|
+
if (Number.isNaN(n)) return '#ffffff';
|
|
208
|
+
const [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
209
|
+
return (r * 299 + g * 587 + b * 114) / 1000 > 150 ? '#09090b' : '#ffffff';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* True for children that must be wrapped in <Text>: strings, numbers and arrays of them —
|
|
214
|
+
* `{count} new` is the array [3, ' new'], and raw text outside <Text> crashes on native.
|
|
215
|
+
*/
|
|
216
|
+
export function isTextNode(node: React.ReactNode): node is string | number | (string | number)[] {
|
|
217
|
+
if (typeof node === 'string' || typeof node === 'number') return true;
|
|
218
|
+
return Array.isArray(node) && node.length > 0 && node.every((n) => typeof n === 'string' || typeof n === 'number');
|
|
219
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "search-bar",
|
|
3
|
+
"type": "component",
|
|
4
|
+
"title": "Search Bar",
|
|
5
|
+
"description": "Search field with a soft focus halo: the magnifier pops on focus, Cancel slides in as the field springs narrower, the clear button scales in and a spinning arc shows loading. Suggestions drop down in an animated panel with recent / trending icons and the matching text in bold; a trailing slot fits a filter or mic button. The minimal variant expands from a round icon.",
|
|
6
|
+
"tier": "free",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"react-native-svg"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"kit"
|
|
12
|
+
],
|
|
13
|
+
"sharedFiles": [
|
|
14
|
+
"lib/kit.tsx"
|
|
15
|
+
],
|
|
16
|
+
"filesDir": "search-bar/files"
|
|
17
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "search-results",
|
|
3
|
+
"type": "block",
|
|
4
|
+
"title": "Search Results",
|
|
5
|
+
"description": "A clean, monochrome search screen: a compact pinned search field, one scrolling row of filter chips (Filters sheet, sort, price, rating, free delivery, open now), tight list rows with neutral thumbnails, rating, distance and price, Recent and Trending while the field is empty, skeleton rows while searching and a compact “No results” state — with a subtle staggered entrance that respects Reduce Motion.",
|
|
6
|
+
"tier": "pro",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"expo-haptics",
|
|
9
|
+
"react-native-svg"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"kit",
|
|
13
|
+
"bottom-sheet",
|
|
14
|
+
"button",
|
|
15
|
+
"radio-group",
|
|
16
|
+
"search-bar",
|
|
17
|
+
"skeleton",
|
|
18
|
+
"slider"
|
|
19
|
+
],
|
|
20
|
+
"sharedFiles": [
|
|
21
|
+
"lib/kit.tsx",
|
|
22
|
+
"components/ui/bottom-sheet.tsx",
|
|
23
|
+
"components/ui/button.tsx",
|
|
24
|
+
"components/ui/radio-group.tsx",
|
|
25
|
+
"components/ui/search-bar.tsx",
|
|
26
|
+
"components/ui/skeleton.tsx",
|
|
27
|
+
"components/ui/slider.tsx"
|
|
28
|
+
],
|
|
29
|
+
"filesDir": null
|
|
30
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import * as Haptics from 'expo-haptics';
|
|
2
|
+
import React, { useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
Animated,
|
|
5
|
+
PanResponder,
|
|
6
|
+
Pressable,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
View,
|
|
9
|
+
type GestureResponderEvent,
|
|
10
|
+
type LayoutChangeEvent,
|
|
11
|
+
type StyleProp,
|
|
12
|
+
type ViewStyle,
|
|
13
|
+
} from 'react-native';
|
|
14
|
+
import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';
|
|
15
|
+
|
|
16
|
+
import { alpha, motion, readableOn, useKit } from '@/lib/kit';
|
|
17
|
+
|
|
18
|
+
export type SegmentedControlVariant = 'pill' | 'underline' | 'outline';
|
|
19
|
+
export type SegmentedControlSize = 'sm' | 'md' | 'lg';
|
|
20
|
+
|
|
21
|
+
export interface SegmentOption {
|
|
22
|
+
value: string;
|
|
23
|
+
label?: string;
|
|
24
|
+
/** Icon node, or a function that receives the current text colour. */
|
|
25
|
+
icon?: React.ReactNode | ((state: { active: boolean; color: string }) => React.ReactNode);
|
|
26
|
+
accessibilityLabel?: string;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SegmentedControlProps {
|
|
31
|
+
/** Segments — plain strings or `{ value, label, icon }` objects. */
|
|
32
|
+
options: (string | SegmentOption)[];
|
|
33
|
+
/** Controlled selection. */
|
|
34
|
+
value?: string;
|
|
35
|
+
/** Initial selection when uncontrolled (default: first segment). */
|
|
36
|
+
defaultValue?: string;
|
|
37
|
+
onValueChange?: (value: string) => void;
|
|
38
|
+
variant?: SegmentedControlVariant;
|
|
39
|
+
size?: SegmentedControlSize;
|
|
40
|
+
/** Equal-width segments that fill the row (default) or segments sized to their content. */
|
|
41
|
+
fullWidth?: boolean;
|
|
42
|
+
/** Indicator colour. Pill uses a raised surface unless an accent is given. */
|
|
43
|
+
accent?: string;
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
/** Selection tick on change (default true). */
|
|
46
|
+
haptics?: boolean;
|
|
47
|
+
accessibilityLabel?: string;
|
|
48
|
+
style?: StyleProp<ViewStyle>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const SIZES: Record<SegmentedControlSize, { h: number; font: number; px: number; icon: number }> = {
|
|
52
|
+
sm: { h: 32, font: 13, px: 12, icon: 14 },
|
|
53
|
+
md: { h: 40, font: 14.5, px: 16, icon: 16 },
|
|
54
|
+
lg: { h: 48, font: 16, px: 20, icon: 18 },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const INSET = 3;
|
|
58
|
+
// Liquid move: the edge in the direction of travel leads, the other one trails behind.
|
|
59
|
+
const LEAD = motion.snappy;
|
|
60
|
+
const TRAIL = { damping: 20, stiffness: 150, mass: 0.9 };
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Segmented control with a liquid indicator: its leading edge springs ahead and the trailing
|
|
64
|
+
* edge catches up, so it stretches as it travels. Measured with onLayout so it fits any label.
|
|
65
|
+
* Drag the selected segment to scrub across the others. Pill (iOS), underline (tabs) and outline
|
|
66
|
+
* looks, with optional icons.
|
|
67
|
+
*/
|
|
68
|
+
export function SegmentedControl({
|
|
69
|
+
options,
|
|
70
|
+
value,
|
|
71
|
+
defaultValue,
|
|
72
|
+
onValueChange,
|
|
73
|
+
variant = 'pill',
|
|
74
|
+
size = 'md',
|
|
75
|
+
fullWidth = true,
|
|
76
|
+
accent,
|
|
77
|
+
disabled,
|
|
78
|
+
haptics = true,
|
|
79
|
+
accessibilityLabel,
|
|
80
|
+
style,
|
|
81
|
+
}: SegmentedControlProps) {
|
|
82
|
+
const { colors, radius, dark } = useKit();
|
|
83
|
+
const gid = 'sc' + useId().replace(/[^a-zA-Z0-9]/g, '');
|
|
84
|
+
const items: SegmentOption[] = options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o));
|
|
85
|
+
const [inner, setInner] = useState(defaultValue ?? items[0]?.value);
|
|
86
|
+
const selected = value ?? inner;
|
|
87
|
+
const index = Math.max(0, items.findIndex((o) => o.value === selected));
|
|
88
|
+
const s = SIZES[size];
|
|
89
|
+
|
|
90
|
+
const layouts = useRef<{ x: number; width: number }[]>([]);
|
|
91
|
+
const [measured, setMeasured] = useState(0);
|
|
92
|
+
// Left and right edges of the indicator, animated separately (JS driver, they set left/width).
|
|
93
|
+
const lx = useRef(new Animated.Value(0)).current;
|
|
94
|
+
const rx = useRef(new Animated.Value(0)).current;
|
|
95
|
+
const placed = useRef(false);
|
|
96
|
+
const shown = useRef(-1);
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const l = layouts.current[index];
|
|
100
|
+
if (!l) return;
|
|
101
|
+
if (!placed.current) {
|
|
102
|
+
lx.setValue(l.x);
|
|
103
|
+
rx.setValue(l.x + l.width);
|
|
104
|
+
placed.current = true;
|
|
105
|
+
shown.current = index;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const right = index >= shown.current;
|
|
109
|
+
shown.current = index;
|
|
110
|
+
Animated.parallel([
|
|
111
|
+
Animated.spring(lx, { toValue: l.x, useNativeDriver: false, ...(right ? TRAIL : LEAD) }),
|
|
112
|
+
Animated.spring(rx, { toValue: l.x + l.width, useNativeDriver: false, ...(right ? LEAD : TRAIL) }),
|
|
113
|
+
]).start();
|
|
114
|
+
}, [index, measured, lx, rx]);
|
|
115
|
+
|
|
116
|
+
const onSegmentLayout = (i: number) => (e: LayoutChangeEvent) => {
|
|
117
|
+
const { x: lx, width } = e.nativeEvent.layout;
|
|
118
|
+
const prev = layouts.current[i];
|
|
119
|
+
if (prev && prev.x === lx && prev.width === width) return;
|
|
120
|
+
layouts.current[i] = { x: lx, width };
|
|
121
|
+
setMeasured((n) => n + 1);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const select = (next: string) => {
|
|
125
|
+
if (next === selected) return;
|
|
126
|
+
if (haptics) Haptics.selectionAsync().catch(() => {});
|
|
127
|
+
if (value === undefined) setInner(next);
|
|
128
|
+
onValueChange?.(next);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Scrub: a horizontal drag that starts on the selected segment slides the selection along.
|
|
132
|
+
// Drags that start elsewhere are left alone, so the control still scrolls inside a ScrollView.
|
|
133
|
+
const live = useRef({ items, select, disabled });
|
|
134
|
+
live.current = { items, select, disabled };
|
|
135
|
+
const touch = useRef({ originX: 0, onActive: false });
|
|
136
|
+
const onSegmentPressIn = (i: number, active: boolean) => (e: GestureResponderEvent) => {
|
|
137
|
+
const l = layouts.current[i];
|
|
138
|
+
touch.current = { originX: e.nativeEvent.pageX - ((l?.x ?? 0) + e.nativeEvent.locationX), onActive: active };
|
|
139
|
+
};
|
|
140
|
+
const scrub = useMemo(
|
|
141
|
+
() =>
|
|
142
|
+
PanResponder.create({
|
|
143
|
+
onMoveShouldSetPanResponderCapture: (_, g) =>
|
|
144
|
+
!live.current.disabled && touch.current.onActive && Math.abs(g.dx) > 8 && Math.abs(g.dx) > Math.abs(g.dy) * 1.5,
|
|
145
|
+
onPanResponderTerminationRequest: () => false,
|
|
146
|
+
onPanResponderRelease: () => (touch.current.onActive = false),
|
|
147
|
+
onPanResponderTerminate: () => (touch.current.onActive = false),
|
|
148
|
+
onPanResponderMove: (_, g) => {
|
|
149
|
+
const at = g.moveX - touch.current.originX;
|
|
150
|
+
const { items: opts, select: pick } = live.current;
|
|
151
|
+
let best = -1;
|
|
152
|
+
let dist = Infinity;
|
|
153
|
+
layouts.current.forEach((l, i) => {
|
|
154
|
+
if (!l || opts[i]?.disabled) return;
|
|
155
|
+
const d = at < l.x ? l.x - at : at > l.x + l.width ? at - l.x - l.width : 0;
|
|
156
|
+
if (d < dist) [best, dist] = [i, d];
|
|
157
|
+
});
|
|
158
|
+
if (best >= 0) pick(opts[best].value);
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
[]
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const tint = accent ?? colors.accent;
|
|
165
|
+
const pill = variant === 'pill';
|
|
166
|
+
const underline = variant === 'underline';
|
|
167
|
+
const outline = variant === 'outline';
|
|
168
|
+
const segRadius = Math.min(radius.md, (s.h - INSET * 2) / 2);
|
|
169
|
+
|
|
170
|
+
const activeFg = pill ? (accent ? readableOn(accent) : colors.foreground) : underline ? colors.foreground : tint;
|
|
171
|
+
const idleFg = colors.mutedForeground;
|
|
172
|
+
|
|
173
|
+
const indicator: ViewStyle = pill
|
|
174
|
+
? {
|
|
175
|
+
top: INSET,
|
|
176
|
+
bottom: INSET,
|
|
177
|
+
borderRadius: segRadius,
|
|
178
|
+
backgroundColor: accent ?? (dark ? colors.input : colors.background),
|
|
179
|
+
borderWidth: accent ? 0 : StyleSheet.hairlineWidth,
|
|
180
|
+
borderColor: alpha(colors.foreground, dark ? 0.1 : 0.06),
|
|
181
|
+
}
|
|
182
|
+
: underline
|
|
183
|
+
? { bottom: 0, height: 3, borderTopLeftRadius: 3, borderTopRightRadius: 3, backgroundColor: tint }
|
|
184
|
+
: {
|
|
185
|
+
top: INSET,
|
|
186
|
+
bottom: INSET,
|
|
187
|
+
borderRadius: segRadius,
|
|
188
|
+
backgroundColor: alpha(tint, 0.12),
|
|
189
|
+
borderWidth: 1.5,
|
|
190
|
+
borderColor: alpha(tint, 0.55),
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// onLayout x includes the track border; absolute children start inside it.
|
|
194
|
+
const trackBorder = pill ? StyleSheet.hairlineWidth : outline ? 1 : 0;
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<View
|
|
198
|
+
{...scrub.panHandlers}
|
|
199
|
+
accessibilityRole="tablist"
|
|
200
|
+
accessibilityLabel={accessibilityLabel}
|
|
201
|
+
style={[
|
|
202
|
+
styles.track,
|
|
203
|
+
{ opacity: disabled ? 0.5 : 1, alignSelf: fullWidth ? 'stretch' : 'flex-start' },
|
|
204
|
+
pill && {
|
|
205
|
+
padding: INSET,
|
|
206
|
+
borderRadius: segRadius + INSET,
|
|
207
|
+
backgroundColor: colors.muted,
|
|
208
|
+
borderWidth: trackBorder,
|
|
209
|
+
borderColor: colors.border,
|
|
210
|
+
},
|
|
211
|
+
outline && { padding: INSET, borderRadius: segRadius + INSET, borderWidth: 1, borderColor: colors.border },
|
|
212
|
+
underline && { borderBottomWidth: StyleSheet.hairlineWidth, borderColor: colors.border },
|
|
213
|
+
style,
|
|
214
|
+
]}
|
|
215
|
+
>
|
|
216
|
+
<Animated.View
|
|
217
|
+
pointerEvents="none"
|
|
218
|
+
style={[
|
|
219
|
+
styles.indicator,
|
|
220
|
+
indicator,
|
|
221
|
+
pill && !accent && styles.shadow,
|
|
222
|
+
{
|
|
223
|
+
left: trackBorder ? Animated.add(lx, -trackBorder) : lx,
|
|
224
|
+
width: Animated.subtract(rx, lx),
|
|
225
|
+
opacity: layouts.current[index] ? 1 : 0,
|
|
226
|
+
},
|
|
227
|
+
]}
|
|
228
|
+
>
|
|
229
|
+
{pill && accent ? (
|
|
230
|
+
// A soft sheen on the filled accent pill.
|
|
231
|
+
<Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
|
|
232
|
+
<Defs>
|
|
233
|
+
<LinearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
|
|
234
|
+
<Stop offset="0" stopColor="#ffffff" stopOpacity={0.26} />
|
|
235
|
+
<Stop offset="0.6" stopColor="#ffffff" stopOpacity={0} />
|
|
236
|
+
</LinearGradient>
|
|
237
|
+
</Defs>
|
|
238
|
+
<Rect width="100%" height="100%" rx={segRadius} fill={`url(#${gid})`} />
|
|
239
|
+
</Svg>
|
|
240
|
+
) : null}
|
|
241
|
+
</Animated.View>
|
|
242
|
+
{items.map((o, i) => (
|
|
243
|
+
<Segment
|
|
244
|
+
key={o.value}
|
|
245
|
+
option={o}
|
|
246
|
+
active={i === index}
|
|
247
|
+
onPress={() => select(o.value)}
|
|
248
|
+
onPressIn={onSegmentPressIn(i, i === index)}
|
|
249
|
+
onLayout={onSegmentLayout(i)}
|
|
250
|
+
fullWidth={fullWidth}
|
|
251
|
+
size={s}
|
|
252
|
+
height={underline ? s.h : s.h - INSET * 2}
|
|
253
|
+
activeFg={activeFg}
|
|
254
|
+
idleFg={idleFg}
|
|
255
|
+
disabled={disabled || o.disabled}
|
|
256
|
+
/>
|
|
257
|
+
))}
|
|
258
|
+
</View>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
interface SegmentProps {
|
|
263
|
+
option: SegmentOption;
|
|
264
|
+
active: boolean;
|
|
265
|
+
onPress: () => void;
|
|
266
|
+
onPressIn: (e: GestureResponderEvent) => void;
|
|
267
|
+
onLayout: (e: LayoutChangeEvent) => void;
|
|
268
|
+
fullWidth: boolean;
|
|
269
|
+
size: { h: number; font: number; px: number; icon: number };
|
|
270
|
+
height: number;
|
|
271
|
+
activeFg: string;
|
|
272
|
+
idleFg: string;
|
|
273
|
+
disabled?: boolean;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function Segment({ option, active, onPress, onPressIn, onLayout, fullWidth, size, height, activeFg, idleFg, disabled }: SegmentProps) {
|
|
277
|
+
// a: text colour (JS). press: content squish, pop: little bounce on selection (native, same view).
|
|
278
|
+
const a = useRef(new Animated.Value(active ? 1 : 0)).current;
|
|
279
|
+
const press = useRef(new Animated.Value(1)).current;
|
|
280
|
+
const pop = useRef(new Animated.Value(1)).current;
|
|
281
|
+
const mounted = useRef(false);
|
|
282
|
+
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
Animated.timing(a, { toValue: active ? 1 : 0, duration: 180, useNativeDriver: false }).start();
|
|
285
|
+
if (active && mounted.current) {
|
|
286
|
+
pop.setValue(0.9);
|
|
287
|
+
Animated.spring(pop, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();
|
|
288
|
+
}
|
|
289
|
+
mounted.current = true;
|
|
290
|
+
}, [active, a, pop]);
|
|
291
|
+
|
|
292
|
+
const pressTo = (to: number) =>
|
|
293
|
+
Animated.spring(press, { toValue: to, useNativeDriver: true, ...(to < 1 ? motion.snappy : motion.bouncy) }).start();
|
|
294
|
+
|
|
295
|
+
const color = a.interpolate({ inputRange: [0, 1], outputRange: [idleFg, activeFg] });
|
|
296
|
+
const icon =
|
|
297
|
+
typeof option.icon === 'function' ? option.icon({ active, color: active ? activeFg : idleFg }) : option.icon;
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<Pressable
|
|
301
|
+
accessibilityRole="tab"
|
|
302
|
+
accessibilityState={{ selected: active, disabled: !!disabled }}
|
|
303
|
+
accessibilityLabel={option.accessibilityLabel ?? option.label}
|
|
304
|
+
disabled={disabled}
|
|
305
|
+
onPress={onPress}
|
|
306
|
+
onPressIn={(e) => {
|
|
307
|
+
onPressIn(e);
|
|
308
|
+
pressTo(0.94);
|
|
309
|
+
}}
|
|
310
|
+
onPressOut={() => pressTo(1)}
|
|
311
|
+
onLayout={onLayout}
|
|
312
|
+
hitSlop={height < 44 ? { top: (44 - height) / 2, bottom: (44 - height) / 2 } : undefined}
|
|
313
|
+
style={[styles.segment, fullWidth && styles.flex, { height, paddingHorizontal: size.px }]}
|
|
314
|
+
>
|
|
315
|
+
<Animated.View style={[styles.content, { transform: [{ scale: Animated.multiply(press, pop) }] }]}>
|
|
316
|
+
{icon ? <View style={{ minWidth: size.icon, alignItems: 'center' }}>{icon}</View> : null}
|
|
317
|
+
{option.label ? (
|
|
318
|
+
<Animated.Text numberOfLines={1} style={[styles.label, { fontSize: size.font, color }]}>
|
|
319
|
+
{option.label}
|
|
320
|
+
</Animated.Text>
|
|
321
|
+
) : null}
|
|
322
|
+
</Animated.View>
|
|
323
|
+
</Pressable>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const styles = StyleSheet.create({
|
|
328
|
+
track: { flexDirection: 'row', alignItems: 'center' },
|
|
329
|
+
flex: { flex: 1 },
|
|
330
|
+
indicator: { position: 'absolute' },
|
|
331
|
+
shadow: {
|
|
332
|
+
shadowColor: '#000',
|
|
333
|
+
shadowOpacity: 0.1,
|
|
334
|
+
shadowRadius: 8,
|
|
335
|
+
shadowOffset: { width: 0, height: 2 },
|
|
336
|
+
elevation: 2,
|
|
337
|
+
},
|
|
338
|
+
segment: { alignItems: 'center', justifyContent: 'center', minWidth: 0 },
|
|
339
|
+
content: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
|
340
|
+
label: { fontWeight: '400', letterSpacing: -0.1, userSelect: 'none' },
|
|
341
|
+
});
|