@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
|
@@ -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}
|
|
@@ -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.
|