@expokit/cli 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lib.mjs +3 -1
- package/package.json +1 -1
- package/registry/_docs/flip-calendar.json +106 -0
- package/registry/_docs/paywall.json +144 -0
- package/registry/_docs/ruler-time-picker.json +147 -0
- package/registry/accordion/files/src/lib/kit.tsx +102 -4
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +8 -11
- package/registry/action-sheet/files/src/lib/kit.tsx +102 -4
- package/registry/alert/files/src/components/ui/alert.tsx +1 -1
- package/registry/alert/files/src/components/ui/button.tsx +5 -18
- package/registry/alert/files/src/lib/kit.tsx +102 -4
- package/registry/avatar/files/src/components/ui/avatar.tsx +5 -8
- package/registry/avatar/files/src/lib/kit.tsx +102 -4
- package/registry/badge/files/src/components/ui/badge.tsx +2 -2
- package/registry/badge/files/src/lib/kit.tsx +102 -4
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +11 -17
- package/registry/bottom-nav/files/src/lib/kit.tsx +102 -4
- package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +7 -14
- package/registry/bottom-sheet/files/src/lib/kit.tsx +102 -4
- package/registry/button/files/src/components/ui/button.tsx +5 -18
- package/registry/button/files/src/lib/kit.tsx +102 -4
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +4 -7
- package/registry/button-3d/files/src/lib/kit.tsx +102 -4
- package/registry/calendar/files/src/components/ui/calendar.tsx +3 -3
- package/registry/calendar/files/src/lib/kit.tsx +102 -4
- package/registry/card/files/src/components/ui/card.tsx +4 -3
- package/registry/card/files/src/lib/kit.tsx +102 -4
- package/registry/checkbox/files/src/lib/kit.tsx +102 -4
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +2 -2
- package/registry/chip-group/files/src/lib/kit.tsx +102 -4
- package/registry/confetti/files/src/lib/kit.tsx +102 -4
- package/registry/coupon/files/src/components/ui/coupon.tsx +5 -10
- package/registry/coupon/files/src/components/ui/flip-card.tsx +3 -9
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +4 -10
- package/registry/coupon/files/src/lib/kit.tsx +102 -4
- package/registry/date-picker/files/src/components/ui/calendar.tsx +3 -3
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +10 -16
- package/registry/date-picker/files/src/lib/kit.tsx +102 -4
- package/registry/dialog/files/src/components/ui/dialog.tsx +6 -12
- package/registry/dialog/files/src/lib/kit.tsx +102 -4
- package/registry/drawer/files/src/components/ui/drawer.tsx +3 -19
- package/registry/drawer/files/src/lib/kit.tsx +102 -4
- package/registry/flip-calendar/files/src/components/ui/flip-calendar.tsx +626 -0
- package/registry/flip-calendar/files/src/lib/kit.tsx +320 -0
- package/registry/flip-calendar.json +17 -0
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +3 -9
- package/registry/flip-card/files/src/lib/kit.tsx +102 -4
- package/registry/floating-input/files/src/lib/kit.tsx +102 -4
- package/registry/grain/files/src/lib/kit.tsx +102 -4
- package/registry/kit/files/src/lib/kit.tsx +102 -4
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +6 -7
- package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +31 -17
- package/registry/list-item/files/src/components/ui/list-item.tsx +1 -1
- package/registry/list-item/files/src/lib/kit.tsx +102 -4
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +1 -1
- package/registry/otp-input/files/src/lib/kit.tsx +102 -4
- package/registry/password-input/files/src/lib/kit.tsx +102 -4
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +3 -9
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +9 -3
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +4 -10
- package/registry/payment-card/files/src/lib/kit.tsx +102 -4
- package/registry/paywall.json +22 -0
- package/registry/progress/files/src/components/ui/progress.tsx +2 -0
- package/registry/progress/files/src/lib/kit.tsx +102 -4
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +1 -1
- package/registry/progress-steps/files/src/lib/kit.tsx +102 -4
- package/registry/radio-group/files/src/lib/kit.tsx +102 -4
- package/registry/ruler-time-picker/files/src/components/ui/ruler-time-picker.tsx +816 -0
- package/registry/ruler-time-picker/files/src/lib/kit.tsx +320 -0
- package/registry/ruler-time-picker.json +19 -0
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +3 -7
- package/registry/search-bar/files/src/lib/kit.tsx +102 -4
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +3 -10
- package/registry/segmented-control/files/src/lib/kit.tsx +102 -4
- package/registry/select/files/src/components/ui/select.tsx +16 -18
- package/registry/select/files/src/lib/kit.tsx +102 -4
- package/registry/skeleton/files/src/lib/kit.tsx +102 -4
- package/registry/slider/files/src/components/ui/slider.tsx +4 -13
- package/registry/slider/files/src/lib/kit.tsx +102 -4
- package/registry/spinner/files/src/lib/kit.tsx +102 -4
- package/registry/stateful-button/files/src/lib/kit.tsx +102 -4
- package/registry/stepper/files/src/components/ui/stepper.tsx +2 -2
- package/registry/stepper/files/src/lib/kit.tsx +102 -4
- package/registry/switch/files/src/components/ui/switch.tsx +2 -5
- package/registry/switch/files/src/lib/kit.tsx +102 -4
- package/registry/tabs/files/src/components/ui/tabs.tsx +2 -2
- package/registry/tabs/files/src/lib/kit.tsx +102 -4
- package/registry/text-animation/files/src/lib/kit.tsx +102 -4
- package/registry/textarea/files/src/lib/kit.tsx +102 -4
- package/registry/ticket-shape/files/src/lib/kit.tsx +102 -4
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +4 -10
- package/registry/tilt-card/files/src/lib/kit.tsx +102 -4
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +16 -14
- package/registry/time-picker/files/src/lib/kit.tsx +102 -4
- package/registry/timeline/files/src/components/ui/avatar.tsx +5 -8
- package/registry/timeline/files/src/components/ui/timeline.tsx +5 -16
- package/registry/timeline/files/src/lib/kit.tsx +102 -4
- package/registry/toast/files/src/components/ui/button.tsx +5 -18
- package/registry/toast/files/src/components/ui/toast.tsx +2 -2
- package/registry/toast/files/src/lib/kit.tsx +102 -4
package/bin/lib.mjs
CHANGED
|
@@ -312,7 +312,7 @@ export function kitVersion(text) {
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
/**
|
|
315
|
-
* Packages a registry item may depend on that work in the Expo Go app (SDK
|
|
315
|
+
* Packages a registry item may depend on that work in the Expo Go app (SDK 58): native modules Expo Go
|
|
316
316
|
* ships, plus pure-JS packages. Anything else needs a development build (`npx expo run:ios|android`).
|
|
317
317
|
* Keep in sync with the Expo Go module list when the SDK moves.
|
|
318
318
|
*/
|
|
@@ -320,8 +320,10 @@ export const EXPO_GO_PACKAGES = new Set([
|
|
|
320
320
|
'@expo/vector-icons',
|
|
321
321
|
'@react-native-async-storage/async-storage',
|
|
322
322
|
'@shopify/react-native-skia',
|
|
323
|
+
'expo-audio',
|
|
323
324
|
'expo-blur',
|
|
324
325
|
'expo-charts',
|
|
326
|
+
'expo-file-system',
|
|
325
327
|
'expo-glass-effect',
|
|
326
328
|
'expo-haptics',
|
|
327
329
|
'expo-router',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expokit/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"mcpName": "io.github.Anshkaran7/expokit",
|
|
5
5
|
"description": "Animated UI components for Expo and React Native — a shadcn-style CLI that copies buttons, inputs, cards, charts and full screens into your app",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flip-calendar",
|
|
3
|
+
"category": "Inputs",
|
|
4
|
+
"aliases": [
|
|
5
|
+
"flip-date-picker",
|
|
6
|
+
"desk-calendar",
|
|
7
|
+
"daily-calendar",
|
|
8
|
+
"page-calendar",
|
|
9
|
+
"tear-off-calendar"
|
|
10
|
+
],
|
|
11
|
+
"exportName": "FlipCalendar",
|
|
12
|
+
"files": [
|
|
13
|
+
"src/components/ui/flip-calendar.tsx"
|
|
14
|
+
],
|
|
15
|
+
"summary": "A desk calendar: lift a page to flip it over the rings, pull down to bring it back.",
|
|
16
|
+
"props": [
|
|
17
|
+
[
|
|
18
|
+
"value",
|
|
19
|
+
"Date",
|
|
20
|
+
"—",
|
|
21
|
+
"Controlled date; its time of day is kept on every change."
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
"defaultValue",
|
|
25
|
+
"Date",
|
|
26
|
+
"today",
|
|
27
|
+
"Initial date when uncontrolled."
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
"onChange",
|
|
31
|
+
"(date: Date) => void",
|
|
32
|
+
"—",
|
|
33
|
+
"Called when a page turns over (next day) or back (previous day)."
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
"minDate",
|
|
37
|
+
"Date",
|
|
38
|
+
"—",
|
|
39
|
+
"Earliest day; pulling back past it only nudges the page."
|
|
40
|
+
],
|
|
41
|
+
[
|
|
42
|
+
"maxDate",
|
|
43
|
+
"Date",
|
|
44
|
+
"—",
|
|
45
|
+
"Latest day; lifting past it only raises the page a little."
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
"locale",
|
|
49
|
+
"string",
|
|
50
|
+
"device",
|
|
51
|
+
"Weekday and month names (Intl), English where Intl isn't available."
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
"accent",
|
|
55
|
+
"string",
|
|
56
|
+
"theme accent",
|
|
57
|
+
"Month band and Sunday colour (try colors.danger for the classic red)."
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
"maxWidth",
|
|
61
|
+
"number",
|
|
62
|
+
"300",
|
|
63
|
+
"Widest the pad grows; it fills its container up to this."
|
|
64
|
+
],
|
|
65
|
+
[
|
|
66
|
+
"haptics",
|
|
67
|
+
"boolean",
|
|
68
|
+
"true",
|
|
69
|
+
"A tick as a page passes upright and a soft thud when it lands."
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
"accessibilityLabel",
|
|
73
|
+
"string",
|
|
74
|
+
"'Date'",
|
|
75
|
+
"Name read by screen readers."
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
"style",
|
|
79
|
+
"StyleProp<ViewStyle>",
|
|
80
|
+
"—",
|
|
81
|
+
"Root container."
|
|
82
|
+
]
|
|
83
|
+
],
|
|
84
|
+
"behavior": [
|
|
85
|
+
[
|
|
86
|
+
"Touch",
|
|
87
|
+
"Drag up to lift today's page; past upright (or with a flick) it turns over to the next day, otherwise it settles back. Drag down to bring yesterday's page back over the rings. The point you grab stays under your finger. A hard flick turns up to six more pages. At minDate / maxDate the page only lifts or nudges a little."
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
"Motion",
|
|
91
|
+
"Pages hinge on the rings in 3D on the native driver, darken as they turn away from the light and swap to their plain back exactly at upright. Every page keeps its own angle, so the page underneath is already in place when one turns. Reduce Motion swaps the turn for an instant change."
|
|
92
|
+
],
|
|
93
|
+
[
|
|
94
|
+
"Screen readers",
|
|
95
|
+
"One adjustable element whose value is the full date; increment turns to the next day, decrement turns back, within the limits."
|
|
96
|
+
]
|
|
97
|
+
],
|
|
98
|
+
"usage": "import { useState } from 'react';\nimport { FlipCalendar } from '@/components/ui/flip-calendar';\n\nconst [date, setDate] = useState(new Date());\n<FlipCalendar value={date} onChange={setDate} minDate={new Date()} />",
|
|
99
|
+
"examples": [
|
|
100
|
+
{
|
|
101
|
+
"name": "Desk",
|
|
102
|
+
"title": "Desk",
|
|
103
|
+
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View } from 'react-native';\nimport { FlipCalendar } from '@/components/ui/flip-calendar';\nimport { useKit } from '@/lib/kit';\n\n/** Controls stop at iOS's largest standard text size; content text stays uncapped. */\nconst FONT_CAP = 1.35;\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\nconst atMidnight = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate());\n\n/** \"Today\", \"Tomorrow\", \"In 5 days\" or \"3 days ago\". */\nfunction relative(date: Date, today: Date) {\n const n = Math.round((atMidnight(date).getTime() - atMidnight(today).getTime()) / 86400000);\n if (n === 0) return 'Today';\n if (n === 1) return 'Tomorrow';\n if (n === -1) return 'Yesterday';\n return n > 0 ? `In ${n} days` : `${-n} days ago`;\n}\n\nconst s = StyleSheet.create({\n frame: { alignSelf: 'stretch', minHeight: 520, alignItems: 'center', justifyContent: 'center', gap: 18, paddingTop: 24, paddingHorizontal: 24 },\n frameScreen: { flex: 1 },\n head: { alignItems: 'center', gap: 2 },\n title: { fontSize: 22, fontWeight: '700' },\n sub: { fontSize: 15 },\n hint: { fontSize: 13, textAlign: 'center' },\n button: { minHeight: 40, paddingHorizontal: 18, alignItems: 'center', justifyContent: 'center', borderWidth: StyleSheet.hairlineWidth },\n buttonText: { fontSize: 14, fontWeight: '600' },\n});\n\nexport default function Desk({ screen, bottomInset = 0 }: DemoProps) {\n const { colors, radius } = useKit();\n const [today] = useState(() => new Date());\n const [date, setDate] = useState(today);\n return (\n <View\n style={[\n s.frame,\n { backgroundColor: colors.muted, paddingBottom: 24 + bottomInset },\n screen ? s.frameScreen : { borderRadius: radius.xl, borderColor: colors.border, borderWidth: StyleSheet.hairlineWidth },\n ]}\n >\n <View style={s.head}>\n <Text style={[s.title, { color: colors.foreground }]}>Pick-up date</Text>\n <Text style={[s.sub, { color: colors.mutedForeground }]}>{relative(date, today)}</Text>\n </View>\n <FlipCalendar value={date} onChange={setDate} accent={colors.danger} accessibilityLabel=\"Pick-up date\" />\n <Text style={[s.hint, { color: colors.mutedForeground }]}>Lift a page to turn it · pull down to go back</Text>\n <Pressable\n accessibilityRole=\"button\"\n onPress={() => setDate(today)}\n style={({ pressed }) => [s.button, { borderColor: colors.border, backgroundColor: colors.card, borderRadius: radius.capsule(40), opacity: pressed ? 0.7 : 1 }]}\n >\n <Text maxFontSizeMultiplier={FONT_CAP} style={[s.buttonText, { color: colors.foreground }]}>Back to today</Text>\n </Pressable>\n </View>\n );\n}\n"
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "paywall",
|
|
3
|
+
"category": "Blocks",
|
|
4
|
+
"aliases": [],
|
|
5
|
+
"exportName": "Paywall",
|
|
6
|
+
"files": [
|
|
7
|
+
"src/components/blocks/paywall.tsx"
|
|
8
|
+
],
|
|
9
|
+
"summary": "Animated paywall, free-trial timeline, success and win-back screens.",
|
|
10
|
+
"props": [
|
|
11
|
+
[
|
|
12
|
+
"variant",
|
|
13
|
+
"\"card\" | \"glow\" | \"trial\"",
|
|
14
|
+
"\"card\"",
|
|
15
|
+
"Gradient card + plan picker, dark full-bleed with an annual toggle, or how the free trial works."
|
|
16
|
+
],
|
|
17
|
+
[
|
|
18
|
+
"appName",
|
|
19
|
+
"string",
|
|
20
|
+
"'Lumen'",
|
|
21
|
+
"Shown on the card and the glow pill."
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
"title",
|
|
25
|
+
"string",
|
|
26
|
+
"per variant",
|
|
27
|
+
"Heading."
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
"subtitle",
|
|
31
|
+
"string",
|
|
32
|
+
"per variant",
|
|
33
|
+
"Line under the heading."
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
"features",
|
|
37
|
+
"{ title, icon? }[]",
|
|
38
|
+
"per variant",
|
|
39
|
+
"Feature grid (card) or pills (glow)."
|
|
40
|
+
],
|
|
41
|
+
[
|
|
42
|
+
"plans",
|
|
43
|
+
"PaywallPlan[]",
|
|
44
|
+
"yearly / monthly / lifetime",
|
|
45
|
+
"{ id, label, price, period, caption?, badge?, trialDays?, cta? }. In glow the toggle picks plans[0] (on) or plans[1] (off)."
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
"plan",
|
|
49
|
+
"string",
|
|
50
|
+
"—",
|
|
51
|
+
"Selected plan id (controlled)."
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
"defaultPlan",
|
|
55
|
+
"string",
|
|
56
|
+
"plans[0].id",
|
|
57
|
+
"Initial plan when uncontrolled."
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
"onPlanChange",
|
|
61
|
+
"(id: string) => void",
|
|
62
|
+
"—",
|
|
63
|
+
"A plan was picked."
|
|
64
|
+
],
|
|
65
|
+
[
|
|
66
|
+
"onPurchase",
|
|
67
|
+
"(plan: PaywallPlan) => void",
|
|
68
|
+
"—",
|
|
69
|
+
"Pressed the main button."
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
"onRestore",
|
|
73
|
+
"() => void",
|
|
74
|
+
"—",
|
|
75
|
+
"Shows “Restore” in the top-left corner."
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
"onClose",
|
|
79
|
+
"() => void",
|
|
80
|
+
"—",
|
|
81
|
+
"Shows a close button."
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
"reminder",
|
|
85
|
+
"boolean",
|
|
86
|
+
"true",
|
|
87
|
+
"trial: the “remind me” switch (controlled with onReminderChange)."
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
"auroraColors",
|
|
91
|
+
"[string, string, string]",
|
|
92
|
+
"orange, violet, cyan",
|
|
93
|
+
"Colours of the moving aurora."
|
|
94
|
+
],
|
|
95
|
+
[
|
|
96
|
+
"accent",
|
|
97
|
+
"string",
|
|
98
|
+
"theme accent",
|
|
99
|
+
"Button and selected-plan colour."
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
"topInset",
|
|
103
|
+
"number",
|
|
104
|
+
"0",
|
|
105
|
+
"Space above the restore / close buttons, e.g. useSafeAreaInsets().top. Also on PaywallSuccess and PaywallWinback."
|
|
106
|
+
],
|
|
107
|
+
[
|
|
108
|
+
"bottomInset",
|
|
109
|
+
"number",
|
|
110
|
+
"30 on iOS, 16 elsewhere",
|
|
111
|
+
"Space under the button, e.g. useSafeAreaInsets().bottom + 12."
|
|
112
|
+
],
|
|
113
|
+
[
|
|
114
|
+
"style",
|
|
115
|
+
"StyleProp<ViewStyle>",
|
|
116
|
+
"—",
|
|
117
|
+
"Root style."
|
|
118
|
+
]
|
|
119
|
+
],
|
|
120
|
+
"behavior": [
|
|
121
|
+
[
|
|
122
|
+
"Motion",
|
|
123
|
+
"The aurora drifts on three loops of different lengths, a sheen sweeps the card, rows rise in one after another and prices slide when the plan changes. Springs are critically damped — nothing bounces."
|
|
124
|
+
],
|
|
125
|
+
[
|
|
126
|
+
"Touch",
|
|
127
|
+
"Drag the card to tilt it; it springs flat when you let go. Plan tiles and rows scale down slightly on press."
|
|
128
|
+
],
|
|
129
|
+
[
|
|
130
|
+
"Reduced motion",
|
|
131
|
+
"Loops, tilt, lifts and press scaling are off; fades and colour changes stay. Confetti fires fewer pieces."
|
|
132
|
+
],
|
|
133
|
+
[
|
|
134
|
+
"Screen readers",
|
|
135
|
+
"Plans and reasons are radio groups and the card reads as one label."
|
|
136
|
+
],
|
|
137
|
+
[
|
|
138
|
+
"Layout",
|
|
139
|
+
"Restore sits top-left and close top-right, clear of the button. The button bar stays pinned and content fades out under it. Pass topInset / bottomInset (e.g. from useSafeAreaInsets) for the notch and home indicator."
|
|
140
|
+
]
|
|
141
|
+
],
|
|
142
|
+
"usage": null,
|
|
143
|
+
"examples": []
|
|
144
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ruler-time-picker",
|
|
3
|
+
"category": "Inputs",
|
|
4
|
+
"aliases": [
|
|
5
|
+
"scrub-time-picker",
|
|
6
|
+
"drag-time-picker",
|
|
7
|
+
"ruler-date-picker",
|
|
8
|
+
"scrub-date-picker",
|
|
9
|
+
"day-ruler"
|
|
10
|
+
],
|
|
11
|
+
"exportName": "RulerTimePicker",
|
|
12
|
+
"files": [
|
|
13
|
+
"src/components/ui/ruler-time-picker.tsx"
|
|
14
|
+
],
|
|
15
|
+
"summary": "Drag a tick ruler and the big time or date rolls piece by piece with a motion blur.",
|
|
16
|
+
"props": [
|
|
17
|
+
[
|
|
18
|
+
"mode",
|
|
19
|
+
"'time' | 'date'",
|
|
20
|
+
"'time'",
|
|
21
|
+
"One tick per `step` minutes, or one tick per day with the 1st of each month marked."
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
"value",
|
|
25
|
+
"{ hours, minutes } | Date",
|
|
26
|
+
"—",
|
|
27
|
+
"Controlled value: a 24-hour time, or a Date in date mode (its time of day is kept)."
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
"defaultValue",
|
|
31
|
+
"{ hours, minutes } | Date",
|
|
32
|
+
"09:00 · today",
|
|
33
|
+
"Initial value when uncontrolled."
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
"onChange",
|
|
37
|
+
"(value) => void",
|
|
38
|
+
"—",
|
|
39
|
+
"Called on every tick the ruler passes, while you drag."
|
|
40
|
+
],
|
|
41
|
+
[
|
|
42
|
+
"label",
|
|
43
|
+
"string",
|
|
44
|
+
"—",
|
|
45
|
+
"Small line above the value, e.g. 'Reminder time:'."
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
"step",
|
|
49
|
+
"number",
|
|
50
|
+
"5",
|
|
51
|
+
"Time: minutes per tick (1–60)."
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
"hour12",
|
|
55
|
+
"boolean",
|
|
56
|
+
"true",
|
|
57
|
+
"Time: 12-hour clock with a rolling am / pm; false for 00–23."
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
"minDate / maxDate",
|
|
61
|
+
"Date",
|
|
62
|
+
"—",
|
|
63
|
+
"Date: earliest / latest pickable day; the ruler resists past them."
|
|
64
|
+
],
|
|
65
|
+
[
|
|
66
|
+
"showYear",
|
|
67
|
+
"'auto' | 'always' | 'never'",
|
|
68
|
+
"'auto'",
|
|
69
|
+
"Date: 'auto' shows the year only when it isn't this year."
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
"locale",
|
|
73
|
+
"string",
|
|
74
|
+
"device",
|
|
75
|
+
"Date: weekday and month names (Intl), English where Intl isn't available."
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
"size",
|
|
79
|
+
"'sm' | 'md' | 'lg'",
|
|
80
|
+
"'md'",
|
|
81
|
+
"Time at 40 / 52 / 64pt, date at 34 / 44 / 54pt."
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
"accent",
|
|
85
|
+
"string",
|
|
86
|
+
"theme accent",
|
|
87
|
+
"Centre needle colour."
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
"haptics",
|
|
91
|
+
"boolean",
|
|
92
|
+
"true",
|
|
93
|
+
"A light tap on every step, a firmer one on each whole hour (time) or new month (date)."
|
|
94
|
+
],
|
|
95
|
+
[
|
|
96
|
+
"sound",
|
|
97
|
+
"boolean",
|
|
98
|
+
"true",
|
|
99
|
+
"A soft tick on every step, generated in code (no audio file); it plays alongside other audio."
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
"accessibilityLabel",
|
|
103
|
+
"string",
|
|
104
|
+
"label",
|
|
105
|
+
"Name read by screen readers."
|
|
106
|
+
],
|
|
107
|
+
[
|
|
108
|
+
"style",
|
|
109
|
+
"StyleProp<ViewStyle>",
|
|
110
|
+
"—",
|
|
111
|
+
"Root container."
|
|
112
|
+
],
|
|
113
|
+
[
|
|
114
|
+
"textStyle",
|
|
115
|
+
"StyleProp<TextStyle>",
|
|
116
|
+
"—",
|
|
117
|
+
"The big value text (colour, weight, font family)."
|
|
118
|
+
]
|
|
119
|
+
],
|
|
120
|
+
"behavior": [
|
|
121
|
+
[
|
|
122
|
+
"Touch",
|
|
123
|
+
"Drag sideways anywhere on the picker to scrub; fling to coast. It snaps to the nearest step when it stops. A touch during a fling catches it. In date mode, past minDate / maxDate the ruler follows at a fraction of your finger and springs back. Every step taps (firmer on the hour or a new month) and ticks softly."
|
|
124
|
+
],
|
|
125
|
+
[
|
|
126
|
+
"Motion",
|
|
127
|
+
"Ticks (and month markers) slide, swell and fade on the native driver from one endless pool. Each piece that changes rolls up (later) or down (earlier) with ghosted copies for a motion blur. Reduce Motion swaps the roll for an instant change."
|
|
128
|
+
],
|
|
129
|
+
[
|
|
130
|
+
"Screen readers",
|
|
131
|
+
"One adjustable element whose value is the spoken time or full date; increment / decrement move one step, within the limits."
|
|
132
|
+
]
|
|
133
|
+
],
|
|
134
|
+
"usage": "import { useState } from 'react';\nimport { RulerTimePicker, type TimeValue } from '@/components/ui/ruler-time-picker';\n\nconst [time, setTime] = useState<TimeValue>({ hours: 2, minutes: 15 });\n<RulerTimePicker label=\"Reminder time:\" value={time} onChange={setTime} />\n\n// One tick per day\nconst [date, setDate] = useState(new Date());\n<RulerTimePicker mode=\"date\" label=\"Delivery date:\" value={date} onChange={setDate} minDate={new Date()} />",
|
|
135
|
+
"examples": [
|
|
136
|
+
{
|
|
137
|
+
"name": "Reminder",
|
|
138
|
+
"title": "Reminder",
|
|
139
|
+
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View } from 'react-native';\nimport { RulerTimePicker, type TimeValue } from '@/components/ui/ruler-time-picker';\nimport { alpha, useKit } from '@/lib/kit';\n\n/** Controls stop at iOS's largest standard text size; content text stays uncapped. */\nconst FONT_CAP = 1.35;\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\nconst BLUR: [number, number][] = [2, 4, 7, 10].flatMap((r, ring) =>\n Array.from({ length: 8 }, (_, i): [number, number] => {\n const a = ((i + ring / 2) / 8) * Math.PI * 2;\n return [Math.cos(a) * r, Math.sin(a) * r];\n }),\n);\n\n/** The settings screen the sheet was opened from, as blocks: a title, a grouped list and toggles. */\nconst SettingsScreen = React.memo(function SettingsScreen() {\n const { colors, radius, dark } = useKit();\n // In dark mode the grouped rows sit a step lighter than the screen, like iOS Settings.\n const group = dark ? colors.secondary : colors.card;\n const rows: [number, 'value' | 'on' | 'off'][] = [\n [0.42, 'value'],\n [0.3, 'value'],\n [0.38, 'on'],\n [0.34, 'on'],\n [0.4, 'off'],\n [0.28, 'value'],\n ];\n const bar = (w: number | `${number}%`, h: number, color: string) => (\n <View style={{ width: w, height: h, backgroundColor: color, borderRadius: radius.capsule(h) }} />\n );\n return (\n <View style={s.screen}>\n {bar(150, 30, alpha(colors.foreground, 0.7))}\n <View style={[s.group, { backgroundColor: group, borderRadius: radius.lg }]}>\n {rows.map(([w, kind], i) => (\n <View key={i} style={[s.row, i > 0 && { borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: colors.border }]}>\n {bar(`${w * 100}%`, 14, alpha(colors.foreground, 0.45))}\n {kind === 'value' ? (\n bar(64, 14, colors.mutedForeground)\n ) : (\n <View style={[s.toggle, { backgroundColor: kind === 'on' ? colors.accent : colors.border, borderRadius: radius.capsule(28) }]}>\n <View style={[s.knob, { backgroundColor: colors.background, alignSelf: kind === 'on' ? 'flex-end' : 'flex-start', borderRadius: radius.capsule(24) }]} />\n </View>\n )}\n </View>\n ))}\n </View>\n <View style={[s.group, s.card2, { backgroundColor: group, borderRadius: radius.lg }]}>\n {bar('55%', 14, alpha(colors.foreground, 0.45))}\n {bar('80%', 12, colors.mutedForeground)}\n </View>\n </View>\n );\n});\n\n/** A bottom sheet over a blurred, dimmed screen, with a Done button under the picker. */\nfunction Sheet({ screen, bottomInset = 0, children }: DemoProps & { children: React.ReactNode }) {\n const { colors, radius, dark } = useKit();\n return (\n <View\n style={[\n s.frame,\n { backgroundColor: colors.muted },\n screen ? s.frameScreen : { borderRadius: radius.xl, borderColor: colors.border, borderWidth: StyleSheet.hairlineWidth },\n ]}\n >\n <View pointerEvents=\"none\" style={StyleSheet.absoluteFill} importantForAccessibility=\"no-hide-descendants\" accessibilityElementsHidden>\n {BLUR.map(([x, y], i) => (\n <View key={i} style={[StyleSheet.absoluteFill, { opacity: 1 / 12, transform: [{ translateX: x }, { translateY: y }] }]}>\n <SettingsScreen />\n </View>\n ))}\n {/* A lighter dim than a modal's, so the screen behind still reads as a screen. */}\n <View style={[StyleSheet.absoluteFill, { backgroundColor: colors.overlay, opacity: dark ? 0.45 : 0.5 }]} />\n </View>\n {/* Raised above the screen: an elevated (lighter) surface in dark mode, and a hairline edge in both. */}\n <View\n style={[\n s.sheet,\n {\n backgroundColor: dark ? colors.border : colors.card,\n borderColor: dark ? alpha(colors.foreground, 0.12) : colors.border,\n borderRadius: radius.scale(32),\n marginBottom: 8 + bottomInset,\n },\n ]}\n >\n <View style={[s.grabber, { backgroundColor: alpha(colors.foreground, dark ? 0.3 : 0.15), borderRadius: radius.capsule(5) }]} />\n {children}\n <Pressable\n accessibilityRole=\"button\"\n style={({ pressed }) => [s.button, { backgroundColor: colors.primary, borderRadius: radius.lg, opacity: pressed ? 0.85 : 1 }]}\n >\n <Text maxFontSizeMultiplier={FONT_CAP} style={[s.buttonText, { color: colors.primaryForeground }]}>Done</Text>\n </Pressable>\n </View>\n </View>\n );\n}\n\nconst s = StyleSheet.create({\n frame: { alignSelf: 'stretch', minHeight: 400, justifyContent: 'flex-end', overflow: 'hidden', paddingHorizontal: 8 },\n frameScreen: { flex: 1 },\n screen: { flex: 1, paddingHorizontal: 16, paddingTop: 20, gap: 14 },\n card2: { paddingVertical: 16, gap: 10 },\n group: { paddingHorizontal: 14 },\n row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', minHeight: 50 },\n toggle: { width: 46, height: 28, padding: 2, justifyContent: 'center' },\n knob: { width: 24, height: 24 },\n sheet: { paddingHorizontal: 16, paddingTop: 10, paddingBottom: 16, gap: 10, borderWidth: StyleSheet.hairlineWidth },\n grabber: { alignSelf: 'center', width: 40, height: 5 },\n button: { minHeight: 52, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 16 },\n buttonText: { fontSize: 16, fontWeight: '600' },\n});\n\nexport default function Reminder(props: DemoProps) {\n const [time, setTime] = useState<TimeValue>({ hours: 2, minutes: 15 });\n return (\n <Sheet {...props}>\n <RulerTimePicker label=\"Reminder time:\" value={time} onChange={setTime} />\n </Sheet>\n );\n}\n"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"name": "Delivery",
|
|
143
|
+
"title": "Delivery",
|
|
144
|
+
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View } from 'react-native';\nimport { RulerTimePicker, type TimeValue } from '@/components/ui/ruler-time-picker';\nimport { alpha, useKit } from '@/lib/kit';\n\n/** Controls stop at iOS's largest standard text size; content text stays uncapped. */\nconst FONT_CAP = 1.35;\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\nconst BLUR: [number, number][] = [2, 4, 7, 10].flatMap((r, ring) =>\n Array.from({ length: 8 }, (_, i): [number, number] => {\n const a = ((i + ring / 2) / 8) * Math.PI * 2;\n return [Math.cos(a) * r, Math.sin(a) * r];\n }),\n);\n\n/** The settings screen the sheet was opened from, as blocks: a title, a grouped list and toggles. */\nconst SettingsScreen = React.memo(function SettingsScreen() {\n const { colors, radius, dark } = useKit();\n // In dark mode the grouped rows sit a step lighter than the screen, like iOS Settings.\n const group = dark ? colors.secondary : colors.card;\n const rows: [number, 'value' | 'on' | 'off'][] = [\n [0.42, 'value'],\n [0.3, 'value'],\n [0.38, 'on'],\n [0.34, 'on'],\n [0.4, 'off'],\n [0.28, 'value'],\n ];\n const bar = (w: number | `${number}%`, h: number, color: string) => (\n <View style={{ width: w, height: h, backgroundColor: color, borderRadius: radius.capsule(h) }} />\n );\n return (\n <View style={s.screen}>\n {bar(150, 30, alpha(colors.foreground, 0.7))}\n <View style={[s.group, { backgroundColor: group, borderRadius: radius.lg }]}>\n {rows.map(([w, kind], i) => (\n <View key={i} style={[s.row, i > 0 && { borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: colors.border }]}>\n {bar(`${w * 100}%`, 14, alpha(colors.foreground, 0.45))}\n {kind === 'value' ? (\n bar(64, 14, colors.mutedForeground)\n ) : (\n <View style={[s.toggle, { backgroundColor: kind === 'on' ? colors.accent : colors.border, borderRadius: radius.capsule(28) }]}>\n <View style={[s.knob, { backgroundColor: colors.background, alignSelf: kind === 'on' ? 'flex-end' : 'flex-start', borderRadius: radius.capsule(24) }]} />\n </View>\n )}\n </View>\n ))}\n </View>\n <View style={[s.group, s.card2, { backgroundColor: group, borderRadius: radius.lg }]}>\n {bar('55%', 14, alpha(colors.foreground, 0.45))}\n {bar('80%', 12, colors.mutedForeground)}\n </View>\n </View>\n );\n});\n\n/** A bottom sheet over a blurred, dimmed screen, with a Done button under the picker. */\nfunction Sheet({ screen, bottomInset = 0, children }: DemoProps & { children: React.ReactNode }) {\n const { colors, radius, dark } = useKit();\n return (\n <View\n style={[\n s.frame,\n { backgroundColor: colors.muted },\n screen ? s.frameScreen : { borderRadius: radius.xl, borderColor: colors.border, borderWidth: StyleSheet.hairlineWidth },\n ]}\n >\n <View pointerEvents=\"none\" style={StyleSheet.absoluteFill} importantForAccessibility=\"no-hide-descendants\" accessibilityElementsHidden>\n {BLUR.map(([x, y], i) => (\n <View key={i} style={[StyleSheet.absoluteFill, { opacity: 1 / 12, transform: [{ translateX: x }, { translateY: y }] }]}>\n <SettingsScreen />\n </View>\n ))}\n {/* A lighter dim than a modal's, so the screen behind still reads as a screen. */}\n <View style={[StyleSheet.absoluteFill, { backgroundColor: colors.overlay, opacity: dark ? 0.45 : 0.5 }]} />\n </View>\n {/* Raised above the screen: an elevated (lighter) surface in dark mode, and a hairline edge in both. */}\n <View\n style={[\n s.sheet,\n {\n backgroundColor: dark ? colors.border : colors.card,\n borderColor: dark ? alpha(colors.foreground, 0.12) : colors.border,\n borderRadius: radius.scale(32),\n marginBottom: 8 + bottomInset,\n },\n ]}\n >\n <View style={[s.grabber, { backgroundColor: alpha(colors.foreground, dark ? 0.3 : 0.15), borderRadius: radius.capsule(5) }]} />\n {children}\n <Pressable\n accessibilityRole=\"button\"\n style={({ pressed }) => [s.button, { backgroundColor: colors.primary, borderRadius: radius.lg, opacity: pressed ? 0.85 : 1 }]}\n >\n <Text maxFontSizeMultiplier={FONT_CAP} style={[s.buttonText, { color: colors.primaryForeground }]}>Done</Text>\n </Pressable>\n </View>\n </View>\n );\n}\n\nconst s = StyleSheet.create({\n frame: { alignSelf: 'stretch', minHeight: 400, justifyContent: 'flex-end', overflow: 'hidden', paddingHorizontal: 8 },\n frameScreen: { flex: 1 },\n screen: { flex: 1, paddingHorizontal: 16, paddingTop: 20, gap: 14 },\n card2: { paddingVertical: 16, gap: 10 },\n group: { paddingHorizontal: 14 },\n row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', minHeight: 50 },\n toggle: { width: 46, height: 28, padding: 2, justifyContent: 'center' },\n knob: { width: 24, height: 24 },\n sheet: { paddingHorizontal: 16, paddingTop: 10, paddingBottom: 16, gap: 10, borderWidth: StyleSheet.hairlineWidth },\n grabber: { alignSelf: 'center', width: 40, height: 5 },\n button: { minHeight: 52, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 16 },\n buttonText: { fontSize: 16, fontWeight: '600' },\n});\n\nexport default function Delivery(props: DemoProps) {\n const [today] = useState(() => new Date());\n const [date, setDate] = useState(() => new Date(today.getFullYear(), today.getMonth(), today.getDate() + 2));\n return (\n <Sheet {...props}>\n <RulerTimePicker mode=\"date\" label=\"Delivery date:\" value={date} onChange={setDate} minDate={today} />\n </Sheet>\n );\n}\n"
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { createContext, useContext, useMemo } from 'react';
|
|
2
|
-
import { Platform, useColorScheme, type TextStyle } from 'react-native';
|
|
2
|
+
import { Platform, useColorScheme, useWindowDimensions, type TextStyle, type ViewStyle } from 'react-native';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* ExpoKit design tokens. Every component reads them through useKit(), so one
|
|
@@ -8,10 +8,10 @@ import { Platform, useColorScheme, type TextStyle } from 'react-native';
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
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.
|
|
11
|
+
* Bumped whenever components start using a new kit API (2: radius.scale / radius.capsule,
|
|
12
|
+
* 3: useLayout / shadow). The CLI won't keep an edited kit.tsx older than the component being added needs.
|
|
13
13
|
*/
|
|
14
|
-
export const KIT_VERSION =
|
|
14
|
+
export const KIT_VERSION = 3;
|
|
15
15
|
|
|
16
16
|
export type ColorScheme = 'light' | 'dark';
|
|
17
17
|
|
|
@@ -58,6 +58,8 @@ export interface Theme {
|
|
|
58
58
|
};
|
|
59
59
|
/** 4pt spacing scale: space(4) === 16. */
|
|
60
60
|
space: (n: number) => number;
|
|
61
|
+
/** The same soft shadow on iOS, Android and web: `style={[styles.card, shadow('md')]}`. */
|
|
62
|
+
shadow: (level: ShadowLevel, color?: string) => ViewStyle;
|
|
61
63
|
font: { family?: string; mono: string };
|
|
62
64
|
text: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl', TextStyle>;
|
|
63
65
|
}
|
|
@@ -134,6 +136,7 @@ function build(scheme: ColorScheme, overrides?: ThemeOverrides): Theme {
|
|
|
134
136
|
capsule: (h) => Math.round((h / 2) * Math.min(1, r / 12)),
|
|
135
137
|
},
|
|
136
138
|
space: (n) => n * 4,
|
|
139
|
+
shadow: (level, color) => shadowFor(level, scheme === 'dark', color),
|
|
137
140
|
font: {
|
|
138
141
|
family: overrides?.fontFamily,
|
|
139
142
|
mono: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'ui-monospace, SFMono-Regular, Menlo, monospace' })!,
|
|
@@ -181,6 +184,101 @@ export function useKit(): Theme {
|
|
|
181
184
|
return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
|
|
182
185
|
}
|
|
183
186
|
|
|
187
|
+
export type ShadowLevel = 'none' | 'sm' | 'md' | 'lg' | 'xl';
|
|
188
|
+
|
|
189
|
+
const SHADOWS: Record<Exclude<ShadowLevel, 'none'>, [y: number, blur: number, opacity: number]> = {
|
|
190
|
+
sm: [1, 3, 0.08],
|
|
191
|
+
md: [4, 14, 0.1],
|
|
192
|
+
lg: [10, 30, 0.14],
|
|
193
|
+
xl: [18, 48, 0.2],
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* A shadow as `boxShadow`, which React Native (0.76+) draws identically on iOS, Android and web —
|
|
198
|
+
* unlike `elevation`, whose Android shadow looks nothing like the iOS one. Darker in dark mode so
|
|
199
|
+
* it still reads on near-black. `color` is a #rrggbb tint (default black).
|
|
200
|
+
*/
|
|
201
|
+
export function shadowFor(level: ShadowLevel, dark: boolean, color = '#000000'): ViewStyle {
|
|
202
|
+
if (level === 'none') return {};
|
|
203
|
+
const [y, blur, opacity] = SHADOWS[level];
|
|
204
|
+
const a = Math.min(0.6, dark ? opacity * 2.8 : opacity);
|
|
205
|
+
const full = color.length === 4 ? '#' + [...color.slice(1)].map((c) => c + c).join('') : color;
|
|
206
|
+
const n = parseInt(full.slice(1, 7), 16);
|
|
207
|
+
const [r, g, b] = Number.isNaN(n) ? [0, 0, 0] : [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
208
|
+
return { boxShadow: `0px ${y}px ${blur}px rgba(${r},${g},${b},${+a.toFixed(3)})` };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Screen size classes, shared by every component so phones, foldables and tablets all get the
|
|
213
|
+
* right layout without any work from you. `compact` — phones (any orientation); `medium` — open
|
|
214
|
+
* foldables (Galaxy Z Fold, Pixel Fold), Surface Duo and small tablets; `expanded` — iPad and
|
|
215
|
+
* larger. Decided by the screen's short side, so a phone on its side stays a phone.
|
|
216
|
+
*/
|
|
217
|
+
export type SizeClass = 'compact' | 'medium' | 'expanded';
|
|
218
|
+
|
|
219
|
+
export const breakpoints = { medium: 600, expanded: 840 };
|
|
220
|
+
|
|
221
|
+
/** Widest a sheet / picker / popover card and a screen's content column get on big screens. */
|
|
222
|
+
export const layoutLimits = { sheet: 560, content: 720, gutter: 24 };
|
|
223
|
+
|
|
224
|
+
export function sizeClassOf(shortSide: number): SizeClass {
|
|
225
|
+
if (!(shortSide >= breakpoints.medium)) return 'compact';
|
|
226
|
+
return shortSide >= breakpoints.expanded ? 'expanded' : 'medium';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface Layout {
|
|
230
|
+
width: number;
|
|
231
|
+
height: number;
|
|
232
|
+
size: SizeClass;
|
|
233
|
+
/** Foldable, tablet or desktop — anything bigger than a phone. */
|
|
234
|
+
wide: boolean;
|
|
235
|
+
landscape: boolean;
|
|
236
|
+
fontScale: number;
|
|
237
|
+
/** Width for a bottom sheet, picker or action sheet: the full screen on a portrait phone, a centred card on anything wider. */
|
|
238
|
+
sheetWidth: number;
|
|
239
|
+
/** Width for a screen's main column (forms, feeds, blocks): full on phones, a readable centred column on tablets. */
|
|
240
|
+
contentWidth: number;
|
|
241
|
+
/** Grid columns that fit the current width: 1 phone, 2 foldable / landscape phone, 3 tablet. */
|
|
242
|
+
columns: number;
|
|
243
|
+
/** A value per size class; a missing one falls back to the next smaller: `pick({ compact: 16, expanded: 24 })`. */
|
|
244
|
+
pick: <T>(values: { compact: T; medium?: T; expanded?: T }) => T;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const finite = (n: number) => (Number.isFinite(n) && n > 0 ? n : 0);
|
|
248
|
+
|
|
249
|
+
/** The layout for a window of `width` × `height` points (the pure part of useLayout). */
|
|
250
|
+
export function layoutFor(rawWidth: number, rawHeight: number, fontScale = 1): Layout {
|
|
251
|
+
const width = finite(rawWidth);
|
|
252
|
+
const height = finite(rawHeight);
|
|
253
|
+
const size = sizeClassOf(Math.min(width, height || width));
|
|
254
|
+
const roomy = width >= breakpoints.medium;
|
|
255
|
+
const { sheet, content, gutter } = layoutLimits;
|
|
256
|
+
return {
|
|
257
|
+
width,
|
|
258
|
+
height,
|
|
259
|
+
size,
|
|
260
|
+
wide: size !== 'compact',
|
|
261
|
+
landscape: width > height,
|
|
262
|
+
fontScale,
|
|
263
|
+
sheetWidth: roomy ? Math.min(sheet, width - gutter * 2) : width,
|
|
264
|
+
contentWidth: roomy ? Math.min(content, width - gutter * 2) : width,
|
|
265
|
+
columns: width >= breakpoints.expanded + 160 ? 3 : roomy ? 2 : 1,
|
|
266
|
+
pick: (values) =>
|
|
267
|
+
(size === 'expanded' ? values.expanded ?? values.medium : size === 'medium' ? values.medium : undefined) ??
|
|
268
|
+
values.compact,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The current screen's layout; re-renders on rotation, split view, Stage Manager and fold / unfold.
|
|
274
|
+
* Components use it so they look right on every device; use it in your own screens the same way:
|
|
275
|
+
* `const { contentWidth } = useLayout()` → `<View style={{ width: '100%', maxWidth: contentWidth, alignSelf: 'center' }}>`.
|
|
276
|
+
*/
|
|
277
|
+
export function useLayout(): Layout {
|
|
278
|
+
const { width, height, fontScale } = useWindowDimensions();
|
|
279
|
+
return useMemo(() => layoutFor(width, height, fontScale), [width, height, fontScale]);
|
|
280
|
+
}
|
|
281
|
+
|
|
184
282
|
/**
|
|
185
283
|
* Shared motion presets so the whole kit moves the same way. Every spring is critically damped
|
|
186
284
|
* (damping ≥ 2·√(stiffness·mass)): it settles smoothly with no overshoot or wobble.
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from 'react-native';
|
|
18
18
|
import Svg, { Path } from 'react-native-svg';
|
|
19
19
|
|
|
20
|
-
import { alpha, motion, readableOn, useKit } from '@/lib/kit';
|
|
20
|
+
import { alpha, motion, readableOn, useKit, useLayout } from '@/lib/kit';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* `inset` — iOS: grouped rounded cards floating above the home indicator, with a separate Cancel card (default).
|
|
@@ -140,6 +140,8 @@ export function ActionSheet({
|
|
|
140
140
|
const { colors, radius, dark, font } = useKit();
|
|
141
141
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
142
142
|
const { height: winH } = useWindowDimensions();
|
|
143
|
+
// Full width on portrait phones; a centred, width-capped card on foldables, tablets and landscape.
|
|
144
|
+
const { sheetWidth } = useLayout();
|
|
143
145
|
const reduceMotion = useReduceMotion();
|
|
144
146
|
const tint = accent ?? colors.accent;
|
|
145
147
|
const [inner, setInner] = useState(defaultOpen);
|
|
@@ -320,7 +322,7 @@ export function ActionSheet({
|
|
|
320
322
|
borderTopLeftRadius: radius.scale(28),
|
|
321
323
|
borderTopRightRadius: radius.scale(28),
|
|
322
324
|
borderColor: edge,
|
|
323
|
-
|
|
325
|
+
boxShadow: `0px -6px 48px rgba(0,0,0,${dark ? 0.5 : 0.12})`,
|
|
324
326
|
},
|
|
325
327
|
]}
|
|
326
328
|
>
|
|
@@ -422,6 +424,7 @@ export function ActionSheet({
|
|
|
422
424
|
}}
|
|
423
425
|
style={[
|
|
424
426
|
styles.sheet,
|
|
427
|
+
{ maxWidth: sheetWidth },
|
|
425
428
|
inset && { paddingHorizontal: 8, paddingBottom: 8 + bottomInset },
|
|
426
429
|
{
|
|
427
430
|
opacity: reduceMotion ? t : 1,
|
|
@@ -459,8 +462,8 @@ function Card({
|
|
|
459
462
|
children: React.ReactNode;
|
|
460
463
|
}) {
|
|
461
464
|
return (
|
|
462
|
-
<View style={
|
|
463
|
-
<View style={
|
|
465
|
+
<View style={{ borderRadius: corner, backgroundColor: bg, boxShadow: `0px 10px 48px rgba(0,0,0,${dark ? 0.5 : 0.14})` }}>
|
|
466
|
+
<View style={{ borderRadius: corner, backgroundColor: bg, boxShadow: `0px 1px 4px rgba(0,0,0,${dark ? 0.3 : 0.06})` }}>
|
|
464
467
|
<View style={{ borderRadius: corner, overflow: 'hidden', borderWidth: StyleSheet.hairlineWidth, borderColor: edge }}>
|
|
465
468
|
{children}
|
|
466
469
|
</View>
|
|
@@ -546,9 +549,7 @@ function useReduceMotion() {
|
|
|
546
549
|
|
|
547
550
|
const styles = StyleSheet.create({
|
|
548
551
|
host: { flex: 1, justifyContent: 'flex-end', alignItems: 'center' },
|
|
549
|
-
sheet: { width: '100%',
|
|
550
|
-
cardAmbient: { shadowColor: '#000', shadowRadius: 24, shadowOffset: { width: 0, height: 10 }, elevation: 10 },
|
|
551
|
-
cardContact: { shadowColor: '#000', shadowRadius: 2, shadowOffset: { width: 0, height: 1 } },
|
|
552
|
+
sheet: { width: '100%', gap: 8 },
|
|
552
553
|
head: { paddingHorizontal: 24, paddingTop: 15, paddingBottom: 14, alignItems: 'center', gap: 4 },
|
|
553
554
|
headLeft: { paddingHorizontal: 20, paddingTop: 6, paddingBottom: 10, gap: 4 },
|
|
554
555
|
title: { fontSize: 13.5, lineHeight: 18, fontWeight: '400', textAlign: 'center' },
|
|
@@ -566,10 +567,6 @@ const styles = StyleSheet.create({
|
|
|
566
567
|
borderWidth: StyleSheet.hairlineWidth,
|
|
567
568
|
borderBottomWidth: 0,
|
|
568
569
|
paddingHorizontal: 8,
|
|
569
|
-
shadowColor: '#000',
|
|
570
|
-
shadowRadius: 24,
|
|
571
|
-
shadowOffset: { width: 0, height: -6 },
|
|
572
|
-
elevation: 16,
|
|
573
570
|
},
|
|
574
571
|
listRow: { minHeight: 56, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, gap: 16 },
|
|
575
572
|
listIcon: { width: 24, alignItems: 'center' },
|