@expokit/cli 0.4.2 → 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/README.md +6 -2
- package/bin/expo-kit.js +26 -3
- package/bin/lib.mjs +17 -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
|
@@ -469,9 +469,8 @@ export function TiltCard({
|
|
|
469
469
|
) : null}
|
|
470
470
|
{/* The card sits in its own (non-collapsing) layer: iOS intersects sibling layers in 3D, so a
|
|
471
471
|
tilted card next to the flat shadow plate would be sliced by it along a diagonal. */}
|
|
472
|
-
{/*
|
|
473
|
-
|
|
474
|
-
<View collapsable={false} style={{ elevation: shadow ? 15 : 0 }}>
|
|
472
|
+
{/* The plate's shadow is a boxShadow (no elevation), so on Android too the card simply paints above it. */}
|
|
473
|
+
<View collapsable={false}>
|
|
475
474
|
<Animated.View
|
|
476
475
|
ref={viewRef}
|
|
477
476
|
{...responder.panHandlers}
|
|
@@ -483,10 +482,8 @@ export function TiltCard({
|
|
|
483
482
|
onAccessibilityAction={(e) => {
|
|
484
483
|
if (e.nativeEvent.actionName === 'activate') onPressRef.current?.();
|
|
485
484
|
}}
|
|
486
|
-
// elevation (no background → no extra shadow) keeps the card above its shadow plate on Android.
|
|
487
485
|
style={{
|
|
488
486
|
borderRadius: radius,
|
|
489
|
-
elevation: shadow ? 15 : 0,
|
|
490
487
|
transform: [{ perspective }, { rotateX }, { rotateY }, { scale }],
|
|
491
488
|
}}
|
|
492
489
|
>
|
|
@@ -665,11 +662,8 @@ const styles = StyleSheet.create({
|
|
|
665
662
|
right: 0,
|
|
666
663
|
bottom: 0,
|
|
667
664
|
backgroundColor: '#000',
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
shadowRadius: 24,
|
|
671
|
-
shadowOffset: { width: 0, height: 16 },
|
|
672
|
-
elevation: 14,
|
|
665
|
+
// boxShadow draws the same soft shadow on iOS, Android and web.
|
|
666
|
+
boxShadow: '0px 16px 48px rgba(0,0,0,0.45)',
|
|
673
667
|
},
|
|
674
668
|
clip: { overflow: 'hidden' },
|
|
675
669
|
window: { position: 'absolute', overflow: 'hidden' },
|
|
@@ -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.
|
|
@@ -839,7 +839,7 @@ const styles = StyleSheet.create({
|
|
|
839
839
|
navs: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
|
840
840
|
nav: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center' },
|
|
841
841
|
todayBtn: { minHeight: 32, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center' },
|
|
842
|
-
todayText: { fontSize: 13, fontWeight: '400' },
|
|
842
|
+
todayText: { fontSize: 13, fontWeight: '400', includeFontPadding: false, textAlignVertical: 'center' },
|
|
843
843
|
weekRow: { flexDirection: 'row', marginBottom: 6 },
|
|
844
844
|
weekday: { flex: 1, textAlign: 'center', fontSize: 13, fontWeight: '400', letterSpacing: 0.2 },
|
|
845
845
|
gridClip: { overflow: 'hidden' },
|
|
@@ -858,7 +858,7 @@ const styles = StyleSheet.create({
|
|
|
858
858
|
alignSelf: 'center',
|
|
859
859
|
},
|
|
860
860
|
fill: { borderWidth: 0 },
|
|
861
|
-
day: { fontSize: 15, fontWeight: '400', fontVariant: ['tabular-nums'] },
|
|
861
|
+
day: { fontSize: 15, fontWeight: '400', fontVariant: ['tabular-nums'], includeFontPadding: false, textAlignVertical: 'center' },
|
|
862
862
|
dayStrong: { fontWeight: '400' },
|
|
863
863
|
outside: { opacity: 0.4 },
|
|
864
864
|
disabledText: { opacity: 0.35 },
|
|
@@ -867,5 +867,5 @@ const styles = StyleSheet.create({
|
|
|
867
867
|
pickGrid: { flexDirection: 'row', flexWrap: 'wrap', alignContent: 'center', paddingTop: 4 },
|
|
868
868
|
pickCell: { width: '33.333%', height: 56, alignItems: 'center', justifyContent: 'center' },
|
|
869
869
|
pickInner: { minWidth: 76, minHeight: 40, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center', borderWidth: 1.5 },
|
|
870
|
-
pickText: { fontSize: 15, fontWeight: '400' },
|
|
870
|
+
pickText: { fontSize: 15, fontWeight: '400', includeFontPadding: false, textAlignVertical: 'center' },
|
|
871
871
|
});
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
type CalendarProps,
|
|
27
27
|
type DateRange,
|
|
28
28
|
} from '@/components/ui/calendar';
|
|
29
|
-
import { alpha, motion, readableOn, useKit } from '@/lib/kit';
|
|
29
|
+
import { alpha, motion, readableOn, useKit, useLayout } from '@/lib/kit';
|
|
30
30
|
|
|
31
31
|
export type { DateRange } from '@/components/ui/calendar';
|
|
32
32
|
export type DatePickerSize = 'sm' | 'md' | 'lg';
|
|
@@ -244,6 +244,7 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
244
244
|
typeof userField.borderRadius === 'number' ? userField.borderRadius : size === 'sm' ? radius.md - 2 : size === 'lg' ? radius.lg : radius.md;
|
|
245
245
|
const restIcon = iconColor ?? colors.mutedForeground;
|
|
246
246
|
const { width: winW, height: winH } = useWindowDimensions();
|
|
247
|
+
const layout = useLayout();
|
|
247
248
|
const ranged = mode === 'range';
|
|
248
249
|
const reduce = useReduceMotion();
|
|
249
250
|
// The pan responder is built once; it reads Reduce Motion through this ref.
|
|
@@ -273,8 +274,8 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
273
274
|
const check = useRef(new Animated.Value(ok ? 1 : 0)).current; // native
|
|
274
275
|
|
|
275
276
|
const asSheet = presentation !== 'popover';
|
|
276
|
-
// Centred dialog instead of a bottom sheet.
|
|
277
|
-
const wide = presentation === 'dialog' || (presentation === 'auto' &&
|
|
277
|
+
// Centred dialog instead of a bottom sheet: on foldables, tablets and landscape phones.
|
|
278
|
+
const wide = presentation === 'dialog' || (presentation === 'auto' && (layout.wide || layout.landscape));
|
|
278
279
|
|
|
279
280
|
useEffect(() => {
|
|
280
281
|
if (reduce) {
|
|
@@ -661,7 +662,7 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
661
662
|
{ backgroundColor: colors.popover, borderColor: colors.border, maxHeight: winH * 0.92 },
|
|
662
663
|
wide
|
|
663
664
|
? { width: Math.min(420, winW - 48), borderRadius: radius.xl, paddingBottom: 14 }
|
|
664
|
-
: { alignSelf: '
|
|
665
|
+
: { width: layout.sheetWidth, alignSelf: 'center', borderTopLeftRadius: radius.xl + 4, borderTopRightRadius: radius.xl + 4, paddingBottom: 30 },
|
|
665
666
|
menuStyle,
|
|
666
667
|
wide
|
|
667
668
|
? {
|
|
@@ -816,11 +817,8 @@ const styles = StyleSheet.create({
|
|
|
816
817
|
centered: { justifyContent: 'center', alignItems: 'center' },
|
|
817
818
|
sheet: {
|
|
818
819
|
borderWidth: StyleSheet.hairlineWidth,
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
shadowRadius: 24,
|
|
822
|
-
shadowOffset: { width: 0, height: -4 },
|
|
823
|
-
elevation: 16,
|
|
820
|
+
// boxShadow, not shadow* / elevation: the same soft shadow on iOS, Android and web.
|
|
821
|
+
boxShadow: '0px -4px 48px rgba(0,0,0,0.18)',
|
|
824
822
|
},
|
|
825
823
|
sheetHeader: { alignItems: 'center', flexShrink: 0, paddingTop: 10, paddingBottom: 8, paddingHorizontal: 16 },
|
|
826
824
|
grabber: { width: 40, height: 5, borderRadius: 3, marginBottom: 12 },
|
|
@@ -830,21 +828,17 @@ const styles = StyleSheet.create({
|
|
|
830
828
|
calendarScroll: { flexGrow: 0, flexShrink: 1 },
|
|
831
829
|
presets: { gap: 8, paddingHorizontal: 16, paddingVertical: 8 },
|
|
832
830
|
preset: { minHeight: 34, paddingHorizontal: 14, borderWidth: 1, alignItems: 'center', justifyContent: 'center' },
|
|
833
|
-
presetText: { fontSize: 13.5, fontWeight: '400' },
|
|
831
|
+
presetText: { fontSize: 13.5, fontWeight: '400', includeFontPadding: false, textAlignVertical: 'center' },
|
|
834
832
|
calendar: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 6 },
|
|
835
833
|
footer: { flexDirection: 'row', flexShrink: 0, gap: 8, paddingTop: 12, paddingHorizontal: 16, borderTopWidth: StyleSheet.hairlineWidth },
|
|
836
834
|
footBtn: { minHeight: 46, paddingHorizontal: 20, alignItems: 'center', justifyContent: 'center' },
|
|
837
835
|
footMain: { flex: 1 },
|
|
838
|
-
footText: { fontSize: 15, fontWeight: '400' },
|
|
836
|
+
footText: { fontSize: 15, fontWeight: '400', includeFontPadding: false, textAlignVertical: 'center' },
|
|
839
837
|
popover: {
|
|
840
838
|
position: 'absolute',
|
|
841
839
|
paddingTop: 10,
|
|
842
840
|
paddingBottom: 10,
|
|
843
841
|
borderWidth: StyleSheet.hairlineWidth,
|
|
844
|
-
|
|
845
|
-
shadowOpacity: 0.16,
|
|
846
|
-
shadowRadius: 20,
|
|
847
|
-
shadowOffset: { width: 0, height: 10 },
|
|
848
|
-
elevation: 14,
|
|
842
|
+
boxShadow: '0px 10px 40px rgba(0,0,0,0.16)',
|
|
849
843
|
},
|
|
850
844
|
});
|
|
@@ -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.
|
|
@@ -476,13 +476,13 @@ export function Dialog({
|
|
|
476
476
|
maxWidth: ios ? 300 : 340,
|
|
477
477
|
borderRadius: corner,
|
|
478
478
|
backgroundColor: colors.popover,
|
|
479
|
-
|
|
479
|
+
boxShadow: `0px 16px 64px rgba(0,0,0,${dark ? 0.55 : 0.18})`,
|
|
480
480
|
},
|
|
481
481
|
lifted,
|
|
482
482
|
style,
|
|
483
483
|
]}
|
|
484
484
|
>
|
|
485
|
-
<View style={
|
|
485
|
+
<View style={{ borderRadius: corner, backgroundColor: colors.popover, boxShadow: `0px 1px 6px rgba(0,0,0,${dark ? 0.3 : 0.06})` }}>
|
|
486
486
|
<View
|
|
487
487
|
onLayout={(e) => {
|
|
488
488
|
const { width: w, height: h } = e.nativeEvent.layout;
|
|
@@ -734,14 +734,8 @@ function useReduceMotion() {
|
|
|
734
734
|
|
|
735
735
|
const styles = StyleSheet.create({
|
|
736
736
|
center: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 },
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
shadowColor: '#000',
|
|
740
|
-
shadowRadius: 32,
|
|
741
|
-
shadowOffset: { width: 0, height: 16 },
|
|
742
|
-
elevation: 18,
|
|
743
|
-
},
|
|
744
|
-
contact: { shadowColor: '#000', shadowRadius: 3, shadowOffset: { width: 0, height: 1 } },
|
|
737
|
+
// Shadows are boxShadow (set inline): the same on iOS, Android and web.
|
|
738
|
+
ambient: { width: '100%' },
|
|
745
739
|
clip: { overflow: 'hidden', borderWidth: StyleSheet.hairlineWidth },
|
|
746
740
|
content: { paddingHorizontal: 22, paddingTop: 24 },
|
|
747
741
|
contentIos: { paddingHorizontal: 18, paddingTop: 20, paddingBottom: 18 },
|
|
@@ -762,8 +756,8 @@ const styles = StyleSheet.create({
|
|
|
762
756
|
flex: { flex: 1 },
|
|
763
757
|
actions: { gap: 10, padding: 22, paddingTop: 22 },
|
|
764
758
|
pill: { minHeight: 48, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 18 },
|
|
765
|
-
pillText: { fontSize: 16, lineHeight: 21, fontWeight: '400', letterSpacing: -0.1 },
|
|
759
|
+
pillText: { fontSize: 16, lineHeight: 21, fontWeight: '400', letterSpacing: -0.1, includeFontPadding: false, textAlignVertical: 'center' },
|
|
766
760
|
iosActions: { borderTopWidth: StyleSheet.hairlineWidth },
|
|
767
761
|
iosButton: { minHeight: 46, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 12 },
|
|
768
|
-
iosText: { fontSize: 17, lineHeight: 22, fontWeight: '400', letterSpacing: -0.3 },
|
|
762
|
+
iosText: { fontSize: 17, lineHeight: 22, fontWeight: '400', letterSpacing: -0.3, includeFontPadding: false, textAlignVertical: 'center' },
|
|
769
763
|
});
|
|
@@ -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.
|
|
@@ -337,13 +337,6 @@ export function Drawer({
|
|
|
337
337
|
styles.panel,
|
|
338
338
|
side === 'left' ? { left: -EXTRA } : { right: -EXTRA },
|
|
339
339
|
{ width: W + EXTRA, opacity: panelOpacity, transform: [{ translateX: panelX }] },
|
|
340
|
-
!push && {
|
|
341
|
-
shadowColor: '#000',
|
|
342
|
-
shadowOpacity: dark ? 0.45 : 0.12,
|
|
343
|
-
shadowRadius: 28,
|
|
344
|
-
shadowOffset: { width: -dir * 8, height: 0 },
|
|
345
|
-
elevation: 24,
|
|
346
|
-
},
|
|
347
340
|
panelStyle,
|
|
348
341
|
]}
|
|
349
342
|
>
|
|
@@ -353,10 +346,8 @@ export function Drawer({
|
|
|
353
346
|
round,
|
|
354
347
|
!push && {
|
|
355
348
|
backgroundColor: surface,
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
shadowRadius: 4,
|
|
359
|
-
shadowOffset: { width: -dir * 1, height: 0 },
|
|
349
|
+
// Ambient + contact shadow as boxShadow: the same on iOS, Android and web.
|
|
350
|
+
boxShadow: `${-dir * 8}px 0px 56px rgba(0,0,0,${dark ? 0.45 : 0.12}), ${-dir}px 0px 8px rgba(0,0,0,${dark ? 0.3 : 0.06})`,
|
|
360
351
|
},
|
|
361
352
|
]}
|
|
362
353
|
>
|
|
@@ -444,13 +435,6 @@ export function Drawer({
|
|
|
444
435
|
style={[
|
|
445
436
|
StyleSheet.absoluteFill,
|
|
446
437
|
screenTransform && { transform: screenTransform },
|
|
447
|
-
push &&
|
|
448
|
-
Platform.OS === 'ios' && {
|
|
449
|
-
shadowColor: '#000',
|
|
450
|
-
shadowOpacity: dark ? 0.5 : 0.16,
|
|
451
|
-
shadowRadius: 24,
|
|
452
|
-
shadowOffset: { width: dir * 6, height: 8 },
|
|
453
|
-
},
|
|
454
438
|
]}
|
|
455
439
|
>
|
|
456
440
|
<Animated.View
|
|
@@ -462,8 +446,8 @@ export function Drawer({
|
|
|
462
446
|
borderWidth: StyleSheet.hairlineWidth,
|
|
463
447
|
borderColor: screenEdge,
|
|
464
448
|
overflow: 'hidden',
|
|
465
|
-
elevation: push ? 16 : 0,
|
|
466
449
|
},
|
|
450
|
+
push && { boxShadow: `${dir * 6}px 8px 48px rgba(0,0,0,${dark ? 0.5 : 0.16})` },
|
|
467
451
|
]}
|
|
468
452
|
>
|
|
469
453
|
{children}
|