@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
|
@@ -371,12 +371,13 @@ export function BottomNav({
|
|
|
371
371
|
) : null;
|
|
372
372
|
|
|
373
373
|
if (look === 'floating') {
|
|
374
|
-
// Two shadows: a wide soft one on the outer view, a tight contact one on the bar itself
|
|
374
|
+
// Two shadows: a wide soft one on the outer view, a tight contact one on the bar itself
|
|
375
|
+
// (boxShadow, so they look the same on iOS, Android and web).
|
|
375
376
|
return (
|
|
376
377
|
<View
|
|
377
378
|
style={[
|
|
378
379
|
styles.floatOuter,
|
|
379
|
-
{ marginBottom: 12 + bottomInset, borderRadius: floatR, backgroundColor: surface,
|
|
380
|
+
{ marginBottom: 12 + bottomInset, borderRadius: floatR, backgroundColor: surface, boxShadow: `0px 12px 48px rgba(0,0,0,${dark ? 0.55 : 0.1})` },
|
|
380
381
|
style,
|
|
381
382
|
]}
|
|
382
383
|
>
|
|
@@ -390,7 +391,7 @@ export function BottomNav({
|
|
|
390
391
|
borderRadius: floatR,
|
|
391
392
|
backgroundColor: surface,
|
|
392
393
|
borderColor: hairline,
|
|
393
|
-
|
|
394
|
+
boxShadow: `0px 1px 6px rgba(0,0,0,${dark ? 0.35 : 0.06})`,
|
|
394
395
|
},
|
|
395
396
|
]}
|
|
396
397
|
>
|
|
@@ -744,7 +745,7 @@ function FabFace({
|
|
|
744
745
|
</Svg>
|
|
745
746
|
</Animated.View>
|
|
746
747
|
) : null}
|
|
747
|
-
<View style={
|
|
748
|
+
<View style={{ width: size, height: size, borderRadius: r, backgroundColor: from, boxShadow: `0px 6px 24px ${alpha(from, 0.4)}` }}>
|
|
748
749
|
<Svg width={size} height={size} style={StyleSheet.absoluteFill}>
|
|
749
750
|
<Defs>
|
|
750
751
|
{twoTone ? (
|
|
@@ -959,16 +960,9 @@ const styles = StyleSheet.create({
|
|
|
959
960
|
},
|
|
960
961
|
floatOuter: {
|
|
961
962
|
marginHorizontal: 16,
|
|
962
|
-
shadowColor: '#000',
|
|
963
|
-
shadowRadius: 24,
|
|
964
|
-
shadowOffset: { width: 0, height: 12 },
|
|
965
|
-
elevation: 10,
|
|
966
963
|
},
|
|
967
964
|
floatInner: {
|
|
968
965
|
borderWidth: StyleSheet.hairlineWidth,
|
|
969
|
-
shadowColor: '#000',
|
|
970
|
-
shadowRadius: 3,
|
|
971
|
-
shadowOffset: { width: 0, height: 1 },
|
|
972
966
|
},
|
|
973
967
|
row: {
|
|
974
968
|
flexDirection: 'row',
|
|
@@ -995,6 +989,8 @@ const styles = StyleSheet.create({
|
|
|
995
989
|
fontWeight: '400',
|
|
996
990
|
marginTop: 4,
|
|
997
991
|
letterSpacing: 0.1,
|
|
992
|
+
includeFontPadding: false,
|
|
993
|
+
textAlignVertical: 'center',
|
|
998
994
|
},
|
|
999
995
|
floatLabel: {
|
|
1000
996
|
position: 'absolute',
|
|
@@ -1013,6 +1009,8 @@ const styles = StyleSheet.create({
|
|
|
1013
1009
|
fontSize: 13.5,
|
|
1014
1010
|
fontWeight: '400',
|
|
1015
1011
|
letterSpacing: -0.1,
|
|
1012
|
+
includeFontPadding: false,
|
|
1013
|
+
textAlignVertical: 'center',
|
|
1016
1014
|
},
|
|
1017
1015
|
measure: {
|
|
1018
1016
|
position: 'absolute',
|
|
@@ -1022,12 +1020,6 @@ const styles = StyleSheet.create({
|
|
|
1022
1020
|
selfStart: {
|
|
1023
1021
|
alignSelf: 'flex-start',
|
|
1024
1022
|
},
|
|
1025
|
-
fab: {
|
|
1026
|
-
shadowOpacity: 0.4,
|
|
1027
|
-
shadowRadius: 12,
|
|
1028
|
-
shadowOffset: { width: 0, height: 6 },
|
|
1029
|
-
elevation: 6,
|
|
1030
|
-
},
|
|
1031
1023
|
home: {
|
|
1032
1024
|
position: 'absolute',
|
|
1033
1025
|
bottom: 6,
|
|
@@ -1059,5 +1051,7 @@ const styles = StyleSheet.create({
|
|
|
1059
1051
|
fontWeight: '400',
|
|
1060
1052
|
lineHeight: 12,
|
|
1061
1053
|
fontVariant: ['tabular-nums'],
|
|
1054
|
+
includeFontPadding: false,
|
|
1055
|
+
textAlignVertical: 'center',
|
|
1062
1056
|
},
|
|
1063
1057
|
});
|
|
@@ -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.
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
type ViewStyle,
|
|
21
21
|
} from 'react-native';
|
|
22
22
|
|
|
23
|
-
import { alpha, motion, useKit } from '@/lib/kit';
|
|
23
|
+
import { alpha, motion, useKit, useLayout } from '@/lib/kit';
|
|
24
24
|
|
|
25
25
|
export type SnapPoint = number | `${number}%`;
|
|
26
26
|
|
|
@@ -126,6 +126,8 @@ export function BottomSheet({
|
|
|
126
126
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
127
127
|
const reduceMotion = useReduceMotion();
|
|
128
128
|
const { height: screen } = useWindowDimensions();
|
|
129
|
+
// Full width on portrait phones; a centred, width-capped card on foldables, tablets and landscape.
|
|
130
|
+
const { sheetWidth } = useLayout();
|
|
129
131
|
|
|
130
132
|
// No snap points: one detent at the content's own height, measured before the sheet slides in.
|
|
131
133
|
const auto = !snapPoints || snapPoints.length === 0;
|
|
@@ -423,8 +425,8 @@ export function BottomSheet({
|
|
|
423
425
|
styles.sheet,
|
|
424
426
|
{
|
|
425
427
|
height: max + OVERDRAG,
|
|
428
|
+
width: sheetWidth,
|
|
426
429
|
opacity: measured ? 1 : 0,
|
|
427
|
-
shadowOpacity: dark ? 0.5 : 0.12,
|
|
428
430
|
transform: [{ translateY: Animated.add(y, lift) }],
|
|
429
431
|
},
|
|
430
432
|
style,
|
|
@@ -439,7 +441,8 @@ export function BottomSheet({
|
|
|
439
441
|
borderTopLeftRadius: corner,
|
|
440
442
|
borderTopRightRadius: corner,
|
|
441
443
|
borderColor: hairline,
|
|
442
|
-
|
|
444
|
+
// Ambient + contact shadow as boxShadow: identical on iOS, Android and web.
|
|
445
|
+
boxShadow: `0px -8px 64px rgba(0,0,0,${dark ? 0.5 : 0.12}), 0px -1px 6px rgba(0,0,0,${dark ? 0.35 : 0.06})`,
|
|
443
446
|
},
|
|
444
447
|
]}
|
|
445
448
|
>
|
|
@@ -549,21 +552,11 @@ const styles = StyleSheet.create({
|
|
|
549
552
|
// No text selection while dragging with a mouse on web.
|
|
550
553
|
grab: Platform.OS === 'web' ? ({ userSelect: 'none', cursor: 'grab' } as object) : {},
|
|
551
554
|
dock: { position: 'absolute', left: 0, right: 0, bottom: -OVERDRAG, alignItems: 'center' },
|
|
552
|
-
sheet: {
|
|
553
|
-
width: '100%',
|
|
554
|
-
maxWidth: 640,
|
|
555
|
-
shadowColor: '#000',
|
|
556
|
-
shadowRadius: 32,
|
|
557
|
-
shadowOffset: { width: 0, height: -8 },
|
|
558
|
-
elevation: 24,
|
|
559
|
-
},
|
|
555
|
+
sheet: { maxWidth: '100%' },
|
|
560
556
|
surface: {
|
|
561
557
|
flex: 1,
|
|
562
558
|
borderWidth: StyleSheet.hairlineWidth,
|
|
563
559
|
borderBottomWidth: 0,
|
|
564
|
-
shadowColor: '#000',
|
|
565
|
-
shadowRadius: 3,
|
|
566
|
-
shadowOffset: { width: 0, height: -1 },
|
|
567
560
|
},
|
|
568
561
|
handleArea: { alignItems: 'center', paddingTop: 8, paddingBottom: 10, minHeight: 28 },
|
|
569
562
|
handle: { width: 36, height: 5 },
|
|
@@ -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.
|
|
@@ -121,7 +121,7 @@ export function Button({
|
|
|
121
121
|
onPressOut,
|
|
122
122
|
...props
|
|
123
123
|
}: ButtonProps) {
|
|
124
|
-
const { colors, radius, dark, font } = useKit();
|
|
124
|
+
const { colors, radius, dark, font, shadow } = useKit();
|
|
125
125
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
126
126
|
const gid = 'btn' + useId().replace(/[^a-zA-Z0-9]/g, '');
|
|
127
127
|
const reduce = useReduceMotion();
|
|
@@ -208,9 +208,9 @@ export function Button({
|
|
|
208
208
|
opacity: isDisabled ? 0.5 : pressed && variant === 'link' ? 0.6 : 1,
|
|
209
209
|
},
|
|
210
210
|
pressed && quiet && variant !== 'link' && { backgroundColor: color ? alpha(tint, 0.1) : colors.muted },
|
|
211
|
-
// Coloured glow
|
|
212
|
-
variant === 'glow' && !isDisabled &&
|
|
213
|
-
variant === 'elevated' &&
|
|
211
|
+
// Coloured glow / soft lift as boxShadow: the same on iOS, Android and web.
|
|
212
|
+
variant === 'glow' && !isDisabled && { boxShadow: `0px 6px 28px ${alpha(tint, 0.45)}` },
|
|
213
|
+
variant === 'elevated' && shadow('md'),
|
|
214
214
|
]}
|
|
215
215
|
{...props}
|
|
216
216
|
>
|
|
@@ -285,19 +285,6 @@ const styles = StyleSheet.create({
|
|
|
285
285
|
base: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
|
|
286
286
|
row: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
|
287
287
|
block: { alignSelf: 'stretch' },
|
|
288
|
-
label: { fontWeight: '400', letterSpacing: -0.1 },
|
|
288
|
+
label: { fontWeight: '400', letterSpacing: -0.1, includeFontPadding: false, textAlignVertical: 'center' },
|
|
289
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
290
|
});
|
|
@@ -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.
|
|
@@ -131,7 +131,7 @@ export function Button3D({
|
|
|
131
131
|
) : isTextNode(children) ? (
|
|
132
132
|
// No numberOfLines: on a narrow fixed-width key ("esc", "Q") the label may use the padding
|
|
133
133
|
// instead of being cut to an ellipsis.
|
|
134
|
-
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: m.ink, fontSize: round ? s.font + 4 : s.font, fontWeight: '600', letterSpacing: 0.1, flexShrink: 0, textAlign: 'center' }, ff]}>
|
|
134
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: m.ink, fontSize: round ? s.font + 4 : s.font, fontWeight: '600', letterSpacing: 0.1, flexShrink: 0, textAlign: 'center', includeFontPadding: false, textAlignVertical: 'center' }, ff]}>
|
|
135
135
|
{children}
|
|
136
136
|
</Text>
|
|
137
137
|
) : (
|
|
@@ -180,15 +180,14 @@ export function Button3D({
|
|
|
180
180
|
top: depth,
|
|
181
181
|
borderRadius: corner,
|
|
182
182
|
backgroundColor: m.edge[1],
|
|
183
|
-
|
|
183
|
+
// boxShadow: the same contact shadow on iOS, Android and web.
|
|
184
|
+
boxShadow: `0px 5px 16px rgba(0,0,0,${m.shadow})`,
|
|
184
185
|
opacity: press.interpolate({ inputRange: [0, 1], outputRange: [1, 0.55] }),
|
|
185
186
|
},
|
|
186
|
-
styles.shadow,
|
|
187
187
|
]}
|
|
188
188
|
/>
|
|
189
189
|
{/* The edge: the keycap's darker side. */}
|
|
190
|
-
{
|
|
191
|
-
<View style={[styles.fill, { top: depth, borderRadius: corner, overflow: 'hidden', elevation: 3 }]}>{gradient(`${gid}-e`, m.edge)}</View>
|
|
190
|
+
<View style={[styles.fill, { top: depth, borderRadius: corner, overflow: 'hidden' }]}>{gradient(`${gid}-e`, m.edge)}</View>
|
|
192
191
|
<Animated.View
|
|
193
192
|
style={[
|
|
194
193
|
styles.face,
|
|
@@ -199,7 +198,6 @@ export function Button3D({
|
|
|
199
198
|
paddingHorizontal: round ? 0 : s.px,
|
|
200
199
|
gap: s.gap,
|
|
201
200
|
borderRadius: corner,
|
|
202
|
-
elevation: 4,
|
|
203
201
|
// Reduce Motion: the key doesn't sink; only the contact shadow dims.
|
|
204
202
|
transform: reduce ? [] : [{ translateY: press.interpolate({ inputRange: [0, 1], outputRange: [0, depth] }) }],
|
|
205
203
|
},
|
|
@@ -235,6 +233,5 @@ function useReduceMotion() {
|
|
|
235
233
|
|
|
236
234
|
const styles = StyleSheet.create({
|
|
237
235
|
fill: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },
|
|
238
|
-
shadow: { shadowColor: '#000', shadowRadius: 8, shadowOffset: { width: 0, height: 5 }, elevation: 3 },
|
|
239
236
|
face: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' },
|
|
240
237
|
});
|