@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.
|
|
@@ -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.
|
|
@@ -149,9 +149,7 @@ export function FlipCard({
|
|
|
149
149
|
) : null}
|
|
150
150
|
{/* Faces live in their own (non-collapsing) layer: iOS intersects sibling layers in 3D, so a
|
|
151
151
|
turning face next to the flat shadow plate would be sliced by it. */}
|
|
152
|
-
{
|
|
153
|
-
get none, since a face turned in 3D would cast a warped shadow wedge below the card. */}
|
|
154
|
-
<View collapsable={false} style={{ elevation: shadow ? 23 : 0 }}>
|
|
152
|
+
<View collapsable={false}>
|
|
155
153
|
{/* The hidden face stays mounted (for the turn) but is hidden from screen readers. */}
|
|
156
154
|
<Animated.View
|
|
157
155
|
style={[styles.face, face(frontTurn, frontOpacity)]}
|
|
@@ -214,11 +212,7 @@ const styles = StyleSheet.create({
|
|
|
214
212
|
right: 0,
|
|
215
213
|
bottom: 0,
|
|
216
214
|
backgroundColor: '#000',
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
shadowRadius: 20,
|
|
220
|
-
shadowOffset: { width: 0, height: 14 },
|
|
221
|
-
// High enough that Android's shadow spreads past the inset plate (iOS uses the shadow* props).
|
|
222
|
-
elevation: 22,
|
|
215
|
+
// boxShadow, not shadow* / elevation: the same soft shadow on iOS, Android and web.
|
|
216
|
+
boxShadow: '0px 14px 40px rgba(0,0,0,0.4)',
|
|
223
217
|
},
|
|
224
218
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
Animated,
|
|
3
4
|
Image,
|
|
4
5
|
Pressable,
|
|
5
6
|
StyleSheet,
|
|
@@ -227,7 +228,7 @@ export function PaymentCard({
|
|
|
227
228
|
labels,
|
|
228
229
|
style,
|
|
229
230
|
}: PaymentCardProps) {
|
|
230
|
-
const { font } = useKit();
|
|
231
|
+
const { font, shadow } = useKit();
|
|
231
232
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
232
233
|
const controlled = flippedProp !== undefined;
|
|
233
234
|
const [own, setOwn] = useState(defaultFlipped);
|
|
@@ -378,7 +379,12 @@ export function PaymentCard({
|
|
|
378
379
|
{card}
|
|
379
380
|
</TiltCard>
|
|
380
381
|
) : (
|
|
381
|
-
<Pressable onPress={toggle} accessibilityRole="button" accessibilityState={{ disabled: frozen }} accessibilityLabel={a11y} style={[
|
|
382
|
+
<Pressable onPress={toggle} accessibilityRole="button" accessibilityState={{ disabled: frozen }} accessibilityLabel={a11y} style={[{ borderRadius: radius }, style]}>
|
|
383
|
+
{/* boxShadow on a plate that narrows with the flip, so the shadow (same on iOS and Android) never shows as an empty box edge-on. */}
|
|
384
|
+
<Animated.View
|
|
385
|
+
pointerEvents="none"
|
|
386
|
+
style={[StyleSheet.absoluteFill, styles.plate, { borderRadius: radius, transform: [{ scaleX: flip.shadowScaleX }] }, shadow('lg')]}
|
|
387
|
+
/>
|
|
382
388
|
{card}
|
|
383
389
|
</Pressable>
|
|
384
390
|
);
|
|
@@ -398,5 +404,5 @@ const styles = StyleSheet.create({
|
|
|
398
404
|
signature: { flex: 1, borderRadius: 3, backgroundColor: '#eef0f4', overflow: 'hidden' },
|
|
399
405
|
cvv: { width: 58, borderRadius: 3, backgroundColor: '#ffffff', alignItems: 'center', justifyContent: 'center' },
|
|
400
406
|
backFoot: { position: 'absolute', left: 18, right: 18, bottom: 16, flexDirection: 'row', alignItems: 'flex-end', gap: 12 },
|
|
401
|
-
|
|
407
|
+
plate: { backgroundColor: '#1c1c20' },
|
|
402
408
|
});
|
|
@@ -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.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "paywall",
|
|
3
|
+
"type": "block",
|
|
4
|
+
"title": "Paywall",
|
|
5
|
+
"description": "Animated paywall screens people actually read. A membership card lit by a drifting aurora (drag it to tilt), plans that tint as you pick them, a dark glow layout with an annual toggle and sliding price, and a free-trial timeline that draws itself. Comes with a “You’re Pro” success screen with confetti and a cancel flow that asks why and offers a reason to stay. Respects Reduce Motion.",
|
|
6
|
+
"tier": "pro",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"react-native-svg",
|
|
9
|
+
"expo-haptics"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"kit",
|
|
13
|
+
"switch",
|
|
14
|
+
"confetti"
|
|
15
|
+
],
|
|
16
|
+
"sharedFiles": [
|
|
17
|
+
"lib/kit.tsx",
|
|
18
|
+
"components/ui/switch.tsx",
|
|
19
|
+
"components/ui/confetti.tsx"
|
|
20
|
+
],
|
|
21
|
+
"filesDir": null
|
|
22
|
+
}
|