@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
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo } from 'react';
|
|
2
|
+
import { Platform, useColorScheme, useWindowDimensions, type TextStyle, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ExpoKit design tokens. Every component reads them through useKit(), so one
|
|
6
|
+
* <KitProvider> (optional) re-skins the whole kit: light / dark, accent colour, radius.
|
|
7
|
+
* Without a provider, components follow the system colour scheme.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
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
|
+
*/
|
|
14
|
+
export const KIT_VERSION = 3;
|
|
15
|
+
|
|
16
|
+
export type ColorScheme = 'light' | 'dark';
|
|
17
|
+
|
|
18
|
+
export interface Colors {
|
|
19
|
+
background: string;
|
|
20
|
+
foreground: string;
|
|
21
|
+
/** Cards, sheets, inputs. */
|
|
22
|
+
card: string;
|
|
23
|
+
cardForeground: string;
|
|
24
|
+
/** Raised surfaces: popovers, toasts. */
|
|
25
|
+
popover: string;
|
|
26
|
+
muted: string;
|
|
27
|
+
mutedForeground: string;
|
|
28
|
+
border: string;
|
|
29
|
+
input: string;
|
|
30
|
+
primary: string;
|
|
31
|
+
primaryForeground: string;
|
|
32
|
+
secondary: string;
|
|
33
|
+
secondaryForeground: string;
|
|
34
|
+
accent: string;
|
|
35
|
+
accentForeground: string;
|
|
36
|
+
success: string;
|
|
37
|
+
warning: string;
|
|
38
|
+
danger: string;
|
|
39
|
+
dangerForeground: string;
|
|
40
|
+
ring: string;
|
|
41
|
+
overlay: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface Theme {
|
|
45
|
+
dark: boolean;
|
|
46
|
+
colors: Colors;
|
|
47
|
+
/** Corner radius scale. `radius.full` makes pills. */
|
|
48
|
+
radius: {
|
|
49
|
+
sm: number;
|
|
50
|
+
md: number;
|
|
51
|
+
lg: number;
|
|
52
|
+
xl: number;
|
|
53
|
+
full: number;
|
|
54
|
+
/** A corner designed at the default radius (12), scaled to the current one: `radius.scale(16)` is 16 at 12, 0 at 0. */
|
|
55
|
+
scale: (n: number) => number;
|
|
56
|
+
/** Corner for a bar or chip of height `h`: a pill at radius ≥ 12, softer below, square at 0. */
|
|
57
|
+
capsule: (h: number) => number;
|
|
58
|
+
};
|
|
59
|
+
/** 4pt spacing scale: space(4) === 16. */
|
|
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;
|
|
63
|
+
font: { family?: string; mono: string };
|
|
64
|
+
text: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl', TextStyle>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const light: Colors = {
|
|
68
|
+
background: '#ffffff',
|
|
69
|
+
foreground: '#09090b',
|
|
70
|
+
card: '#ffffff',
|
|
71
|
+
cardForeground: '#09090b',
|
|
72
|
+
popover: '#ffffff',
|
|
73
|
+
muted: '#f4f4f5',
|
|
74
|
+
mutedForeground: '#71717a',
|
|
75
|
+
border: '#e4e4e7',
|
|
76
|
+
input: '#d4d4d8',
|
|
77
|
+
primary: '#18181b',
|
|
78
|
+
primaryForeground: '#fafafa',
|
|
79
|
+
secondary: '#f4f4f5',
|
|
80
|
+
secondaryForeground: '#18181b',
|
|
81
|
+
accent: '#18181b',
|
|
82
|
+
accentForeground: '#ffffff',
|
|
83
|
+
success: '#16a34a',
|
|
84
|
+
warning: '#f59e0b',
|
|
85
|
+
danger: '#e5484d',
|
|
86
|
+
dangerForeground: '#ffffff',
|
|
87
|
+
ring: '#18181b',
|
|
88
|
+
overlay: 'rgba(0,0,0,0.4)',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const dark: Colors = {
|
|
92
|
+
background: '#09090b',
|
|
93
|
+
foreground: '#fafafa',
|
|
94
|
+
card: '#111113',
|
|
95
|
+
cardForeground: '#fafafa',
|
|
96
|
+
popover: '#18181b',
|
|
97
|
+
muted: '#1c1c20',
|
|
98
|
+
mutedForeground: '#a1a1aa',
|
|
99
|
+
border: '#27272a',
|
|
100
|
+
input: '#34343a',
|
|
101
|
+
primary: '#fafafa',
|
|
102
|
+
primaryForeground: '#18181b',
|
|
103
|
+
secondary: '#1f1f23',
|
|
104
|
+
secondaryForeground: '#fafafa',
|
|
105
|
+
accent: '#fafafa',
|
|
106
|
+
accentForeground: '#09090b',
|
|
107
|
+
success: '#3dd68c',
|
|
108
|
+
warning: '#ffb224',
|
|
109
|
+
danger: '#ff6369',
|
|
110
|
+
dangerForeground: '#ffffff',
|
|
111
|
+
ring: '#d4d4d8',
|
|
112
|
+
overlay: 'rgba(0,0,0,0.6)',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const palettes = { light, dark };
|
|
116
|
+
|
|
117
|
+
function build(scheme: ColorScheme, overrides?: ThemeOverrides): Theme {
|
|
118
|
+
const base = scheme === 'dark' ? dark : light;
|
|
119
|
+
const accent = overrides?.accent;
|
|
120
|
+
const colors: Colors = {
|
|
121
|
+
...base,
|
|
122
|
+
...(accent ? { accent, ring: accent, accentForeground: readableOn(accent) } : null),
|
|
123
|
+
...overrides?.colors?.[scheme],
|
|
124
|
+
};
|
|
125
|
+
const r = overrides?.radius ?? 12;
|
|
126
|
+
return {
|
|
127
|
+
dark: scheme === 'dark',
|
|
128
|
+
colors,
|
|
129
|
+
radius: {
|
|
130
|
+
sm: Math.round(r * 0.5),
|
|
131
|
+
md: r,
|
|
132
|
+
lg: Math.round(r * 1.35),
|
|
133
|
+
xl: Math.round(r * 1.8),
|
|
134
|
+
full: 999,
|
|
135
|
+
scale: (n) => Math.round((n * r) / 12),
|
|
136
|
+
capsule: (h) => Math.round((h / 2) * Math.min(1, r / 12)),
|
|
137
|
+
},
|
|
138
|
+
space: (n) => n * 4,
|
|
139
|
+
shadow: (level, color) => shadowFor(level, scheme === 'dark', color),
|
|
140
|
+
font: {
|
|
141
|
+
family: overrides?.fontFamily,
|
|
142
|
+
mono: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'ui-monospace, SFMono-Regular, Menlo, monospace' })!,
|
|
143
|
+
},
|
|
144
|
+
text: {
|
|
145
|
+
xs: { fontSize: 12, lineHeight: 16 },
|
|
146
|
+
sm: { fontSize: 14, lineHeight: 20 },
|
|
147
|
+
md: { fontSize: 16, lineHeight: 22 },
|
|
148
|
+
lg: { fontSize: 18, lineHeight: 24, fontWeight: '400' },
|
|
149
|
+
xl: { fontSize: 22, lineHeight: 28, fontWeight: '400', letterSpacing: -0.3 },
|
|
150
|
+
xxl: { fontSize: 30, lineHeight: 36, fontWeight: '400', letterSpacing: -0.8 },
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface ThemeOverrides {
|
|
156
|
+
/** Brand colour used for focus rings, selected states and accent buttons. */
|
|
157
|
+
accent?: string;
|
|
158
|
+
/** Base corner radius in points (default 12). */
|
|
159
|
+
radius?: number;
|
|
160
|
+
fontFamily?: string;
|
|
161
|
+
/** Override any token per scheme. */
|
|
162
|
+
colors?: Partial<Record<ColorScheme, Partial<Colors>>>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const KitContext = createContext<{ scheme?: ColorScheme; overrides?: ThemeOverrides } | null>(null);
|
|
166
|
+
|
|
167
|
+
export function KitProvider({
|
|
168
|
+
scheme,
|
|
169
|
+
children,
|
|
170
|
+
...overrides
|
|
171
|
+
}: ThemeOverrides & { scheme?: ColorScheme | 'system'; children: React.ReactNode }) {
|
|
172
|
+
const value = useMemo(
|
|
173
|
+
() => ({ scheme: scheme === 'system' ? undefined : scheme, overrides }),
|
|
174
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
175
|
+
[scheme, overrides.accent, overrides.radius, overrides.fontFamily, overrides.colors]
|
|
176
|
+
);
|
|
177
|
+
return <KitContext.Provider value={value}>{children}</KitContext.Provider>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function useKit(): Theme {
|
|
181
|
+
const ctx = useContext(KitContext);
|
|
182
|
+
const system = useColorScheme();
|
|
183
|
+
const scheme: ColorScheme = ctx?.scheme ?? (system === 'dark' ? 'dark' : 'light');
|
|
184
|
+
return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
|
|
185
|
+
}
|
|
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
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Shared motion presets so the whole kit moves the same way. Every spring is critically damped
|
|
284
|
+
* (damping ≥ 2·√(stiffness·mass)): it settles smoothly with no overshoot or wobble.
|
|
285
|
+
*/
|
|
286
|
+
export const motion = {
|
|
287
|
+
/** Toggles, selection indicators. */
|
|
288
|
+
snappy: { damping: 30, stiffness: 260, mass: 0.8 },
|
|
289
|
+
/** Press feedback, pops. (Kept under its old name; it no longer bounces.) */
|
|
290
|
+
bouncy: { damping: 26, stiffness: 220, mass: 0.7 },
|
|
291
|
+
/** Sheets, layout changes. */
|
|
292
|
+
gentle: { damping: 27, stiffness: 160, mass: 1 },
|
|
293
|
+
fast: 160,
|
|
294
|
+
normal: 240,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/** Adds alpha to a #rrggbb colour: alpha('#6366f1', 0.2). */
|
|
298
|
+
export function alpha(hex: string, a: number) {
|
|
299
|
+
if (!hex.startsWith('#') || (hex.length !== 7 && hex.length !== 4)) return hex;
|
|
300
|
+
const full = hex.length === 4 ? '#' + [...hex.slice(1)].map((c) => c + c).join('') : hex;
|
|
301
|
+
return full + Math.round(Math.max(0, Math.min(1, a)) * 255).toString(16).padStart(2, '0');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Black or white, whichever reads better on `hex`. */
|
|
305
|
+
export function readableOn(hex: string) {
|
|
306
|
+
const full = hex.length === 4 ? '#' + [...hex.slice(1)].map((c) => c + c).join('') : hex;
|
|
307
|
+
const n = parseInt(full.slice(1, 7), 16);
|
|
308
|
+
if (Number.isNaN(n)) return '#ffffff';
|
|
309
|
+
const [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
310
|
+
return (r * 299 + g * 587 + b * 114) / 1000 > 150 ? '#09090b' : '#ffffff';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* True for children that must be wrapped in <Text>: strings, numbers and arrays of them —
|
|
315
|
+
* `{count} new` is the array [3, ' new'], and raw text outside <Text> crashes on native.
|
|
316
|
+
*/
|
|
317
|
+
export function isTextNode(node: React.ReactNode): node is string | number | (string | number)[] {
|
|
318
|
+
if (typeof node === 'string' || typeof node === 'number') return true;
|
|
319
|
+
return Array.isArray(node) && node.length > 0 && node.every((n) => typeof n === 'string' || typeof n === 'number');
|
|
320
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ruler-time-picker",
|
|
3
|
+
"type": "component",
|
|
4
|
+
"title": "Ruler Time Picker",
|
|
5
|
+
"description": "A time or date picker you actually want to drag: scrub or fling a tick ruler and every piece of the big value rolls to its next one with a motion blur. Ticks swell in a bell around the needle, the bump trails your drag, and it snaps to a step when you let go — with a selection haptic on every tick. mode=\"date\" makes each tick a day, marks the 1st of every month and pulls back like a rubber band past min / max dates.",
|
|
6
|
+
"tier": "free",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"expo-haptics",
|
|
9
|
+
"expo-audio",
|
|
10
|
+
"expo-file-system"
|
|
11
|
+
],
|
|
12
|
+
"registryDependencies": [
|
|
13
|
+
"kit"
|
|
14
|
+
],
|
|
15
|
+
"sharedFiles": [
|
|
16
|
+
"lib/kit.tsx"
|
|
17
|
+
],
|
|
18
|
+
"filesDir": "ruler-time-picker/files"
|
|
19
|
+
}
|
|
@@ -434,7 +434,7 @@ function SuggestionPanel({
|
|
|
434
434
|
reduce: boolean;
|
|
435
435
|
onSelect: (item: SearchSuggestion) => void;
|
|
436
436
|
}) {
|
|
437
|
-
const { colors, radius, font } = useKit();
|
|
437
|
+
const { colors, radius, font, shadow } = useKit();
|
|
438
438
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
439
439
|
const [mounted, setMounted] = useState(open);
|
|
440
440
|
const enter = useRef(new Animated.Value(0)).current;
|
|
@@ -459,6 +459,7 @@ function SuggestionPanel({
|
|
|
459
459
|
accessibilityRole="list"
|
|
460
460
|
style={[
|
|
461
461
|
styles.panel,
|
|
462
|
+
shadow('md'),
|
|
462
463
|
{
|
|
463
464
|
backgroundColor: colors.popover,
|
|
464
465
|
borderColor: colors.border,
|
|
@@ -565,18 +566,13 @@ const styles = StyleSheet.create({
|
|
|
565
566
|
clearDot: { alignItems: 'center', justifyContent: 'center' },
|
|
566
567
|
trailing: { paddingLeft: 2, alignItems: 'center', justifyContent: 'center' },
|
|
567
568
|
cancel: { position: 'absolute', left: 0, top: 0, bottom: 0, justifyContent: 'center', paddingLeft: 14 },
|
|
568
|
-
cancelText: { fontWeight: '400' },
|
|
569
|
+
cancelText: { fontWeight: '400', includeFontPadding: false, textAlignVertical: 'center' },
|
|
569
570
|
measure: { position: 'absolute', opacity: 0, left: 0, top: 0 },
|
|
570
571
|
panel: {
|
|
571
572
|
marginTop: 10,
|
|
572
573
|
paddingVertical: 6,
|
|
573
574
|
borderWidth: StyleSheet.hairlineWidth * 2,
|
|
574
575
|
overflow: 'hidden',
|
|
575
|
-
shadowColor: '#000',
|
|
576
|
-
shadowOpacity: 0.08,
|
|
577
|
-
shadowRadius: 16,
|
|
578
|
-
shadowOffset: { width: 0, height: 8 },
|
|
579
|
-
elevation: 4,
|
|
580
576
|
},
|
|
581
577
|
option: { flexDirection: 'row', alignItems: 'center', gap: 12, minHeight: 44, paddingHorizontal: 12 },
|
|
582
578
|
optionIcon: { width: 28, height: 28, borderRadius: 14, alignItems: 'center', justifyContent: 'center' },
|
|
@@ -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.
|
|
@@ -84,7 +84,7 @@ export function SegmentedControl({
|
|
|
84
84
|
accessibilityLabel,
|
|
85
85
|
style,
|
|
86
86
|
}: SegmentedControlProps) {
|
|
87
|
-
const { colors, radius, dark } = useKit();
|
|
87
|
+
const { colors, radius, dark, shadow } = useKit();
|
|
88
88
|
const gid = 'sc' + useId().replace(/[^a-zA-Z0-9]/g, '');
|
|
89
89
|
const reduce = useReduceMotion();
|
|
90
90
|
const items: SegmentOption[] = options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o));
|
|
@@ -225,7 +225,7 @@ export function SegmentedControl({
|
|
|
225
225
|
style={[
|
|
226
226
|
styles.indicator,
|
|
227
227
|
indicator,
|
|
228
|
-
pill && !accent &&
|
|
228
|
+
pill && !accent && shadow('md'),
|
|
229
229
|
{
|
|
230
230
|
left: trackBorder ? Animated.add(lx, -trackBorder) : lx,
|
|
231
231
|
width: Animated.subtract(rx, lx),
|
|
@@ -358,14 +358,7 @@ const styles = StyleSheet.create({
|
|
|
358
358
|
track: { flexDirection: 'row', alignItems: 'center' },
|
|
359
359
|
flex: { flex: 1 },
|
|
360
360
|
indicator: { position: 'absolute' },
|
|
361
|
-
shadow: {
|
|
362
|
-
shadowColor: '#000',
|
|
363
|
-
shadowOpacity: 0.1,
|
|
364
|
-
shadowRadius: 8,
|
|
365
|
-
shadowOffset: { width: 0, height: 2 },
|
|
366
|
-
elevation: 2,
|
|
367
|
-
},
|
|
368
361
|
segment: { alignItems: 'center', justifyContent: 'center', minWidth: 0 },
|
|
369
362
|
content: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
|
370
|
-
label: { fontWeight: '400', letterSpacing: -0.1, userSelect: 'none' },
|
|
363
|
+
label: { fontWeight: '400', letterSpacing: -0.1, userSelect: 'none', includeFontPadding: false, textAlignVertical: 'center' },
|
|
371
364
|
});
|