@expokit/cli 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/bin/lib.mjs +1 -1
- package/bin/liquidglass.mjs +281 -0
- package/package.json +17 -3
- package/registry/_docs/accordion.json +1 -1
- package/registry/_docs/avatar.json +6 -6
- package/registry/_docs/badge.json +3 -3
- package/registry/_docs/bottom-nav.json +6 -6
- package/registry/_docs/bottom-sheet.json +3 -3
- package/registry/_docs/bullet.json +6 -0
- package/registry/_docs/button-3d.json +2 -2
- package/registry/_docs/button.json +2 -2
- package/registry/_docs/calendar.json +5 -5
- package/registry/_docs/card.json +1 -1
- package/registry/_docs/chip-group.json +1 -1
- package/registry/_docs/confetti.json +3 -3
- package/registry/_docs/date-picker.json +4 -4
- package/registry/_docs/drawer.json +4 -4
- package/registry/_docs/flip-card.json +4 -4
- package/registry/_docs/floating-input.json +1 -1
- package/registry/_docs/grain.json +2 -2
- package/registry/_docs/isometric-bar.json +6 -0
- package/registry/_docs/liquidglass-tab.json +2 -1
- package/registry/_docs/otp-input.json +1 -1
- package/registry/_docs/payment-card.json +3 -3
- package/registry/_docs/pixel-bar.json +6 -0
- package/registry/_docs/polar-area.json +6 -0
- package/registry/_docs/progress-steps.json +4 -4
- package/registry/_docs/progress.json +2 -2
- package/registry/_docs/radio-group.json +1 -1
- package/registry/_docs/search-bar.json +3 -3
- package/registry/_docs/segmented-control.json +1 -1
- package/registry/_docs/select.json +6 -6
- package/registry/_docs/skeleton.json +1 -1
- package/registry/_docs/slider.json +6 -6
- package/registry/_docs/spinner.json +2 -2
- package/registry/_docs/stateful-button.json +1 -1
- package/registry/_docs/stepper.json +3 -3
- package/registry/_docs/tabs.json +2 -2
- package/registry/_docs/text-animation.json +4 -4
- package/registry/_docs/textarea.json +1 -1
- package/registry/_docs/tilt-card.json +3 -3
- package/registry/_docs/time-picker.json +6 -6
- package/registry/_docs/timeline-chart.json +6 -0
- package/registry/_docs/timeline.json +4 -4
- package/registry/_docs/treemap.json +6 -0
- package/registry/_docs/waterfall.json +6 -0
- package/registry/accordion/files/src/components/ui/accordion.tsx +31 -3
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +4 -1
- package/registry/alert/files/src/components/ui/alert.tsx +5 -2
- package/registry/alert/files/src/components/ui/button.tsx +32 -4
- package/registry/badge/files/src/components/ui/badge.tsx +2 -2
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +11 -5
- package/registry/button/files/src/components/ui/button.tsx +32 -4
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +33 -6
- package/registry/calendar/files/src/components/ui/calendar.tsx +62 -17
- package/registry/card/files/src/components/ui/card.tsx +25 -5
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +26 -4
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +35 -5
- package/registry/coupon/files/src/components/ui/coupon.tsx +45 -21
- package/registry/coupon/files/src/components/ui/flip-card.tsx +22 -3
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +57 -16
- package/registry/date-picker/files/src/components/ui/calendar.tsx +62 -17
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +96 -42
- package/registry/dialog/files/src/components/ui/dialog.tsx +9 -5
- package/registry/drawer/files/src/components/ui/drawer.tsx +8 -4
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +22 -3
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +75 -26
- package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +3 -94
- package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +2 -1
- package/registry/liquidglass-tab/files/src/app/_layout.tsx +2 -2
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +2 -1
- package/registry/liquidglass-tab/files/src/components/LiquidGlassTabs.tsx +110 -0
- package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +31 -6
- package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +12 -4
- package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +6 -3
- package/registry/liquidglass-tab/files/src/lib/tabs.ts +4 -2
- package/registry/list-item/files/src/components/ui/list-item.tsx +10 -3
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +50 -19
- package/registry/password-input/files/src/components/ui/floating-input.tsx +75 -26
- package/registry/password-input/files/src/components/ui/password-input.tsx +53 -19
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +22 -3
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +19 -15
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +57 -16
- package/registry/progress/files/src/components/ui/progress.tsx +9 -2
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +11 -4
- package/registry/radio-group/files/src/components/ui/radio-group.tsx +39 -9
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +70 -17
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +37 -9
- package/registry/select/files/src/components/ui/select.tsx +158 -83
- package/registry/slider/files/src/components/ui/slider.tsx +53 -25
- package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +12 -4
- package/registry/stepper/files/src/components/ui/stepper.tsx +76 -26
- package/registry/switch/files/src/components/ui/switch.tsx +56 -19
- package/registry/tabs/files/src/components/ui/tabs.tsx +7 -4
- package/registry/text-animation/files/src/components/ui/text-animation.tsx +7 -2
- package/registry/textarea/files/src/components/ui/textarea.tsx +104 -36
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +57 -16
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +154 -73
- package/registry/timeline/files/src/components/ui/timeline.tsx +9 -4
- package/registry/toast/files/src/components/ui/button.tsx +32 -4
- package/registry/toast/files/src/components/ui/toast.tsx +15 -6
package/README.md
CHANGED
|
@@ -9,6 +9,23 @@
|
|
|
9
9
|
The CLI for **[ExpoKit](https://expokit.in)** — animated UI components for Expo and React Native. It copies
|
|
10
10
|
the component source into your app (like shadcn/ui), so you own the code.
|
|
11
11
|
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://expokit.in/showcase"><img src="https://expokit.in/readme/charts.gif" alt="ExpoKit charts on iPhone: scrubbing an area chart and a bar chart, dragging a gauge, tapping through a donut" width="720"></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="https://expokit.in/docs/components"><img src="https://expokit.in/readme/screens.jpg" alt="ExpoKit on iPhone in dark and light mode: payment card, flip card, dashboard block and checkout block" width="720"></a>
|
|
18
|
+
<br>
|
|
19
|
+
<sub>Payment card and flip card are free; the dashboard and checkout screens are <a href="https://expokit.in/pricing">Pro</a> blocks.</sub>
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
- **Buttons, inputs, cards, sheets, tabs, charts and full screens**, all animated, with light and dark mode
|
|
23
|
+
- **Built for touch**: springs, drag gestures and haptics, on iOS, Android, web and in Expo Go
|
|
24
|
+
- **You own the code**: plain typed files in your app, with no runtime library to lock you in
|
|
25
|
+
|
|
26
|
+
**[Browse every component with live previews →](https://expokit.in/docs/components)** ·
|
|
27
|
+
**[Try them on your phone in Expo Go →](https://expokit.in/docs/run-on-your-phone)**
|
|
28
|
+
|
|
12
29
|
## Usage
|
|
13
30
|
|
|
14
31
|
In your Expo app's folder:
|
package/bin/lib.mjs
CHANGED
|
@@ -312,7 +312,7 @@ export function kitVersion(text) {
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
/**
|
|
315
|
-
* Packages a registry item may depend on that work in the Expo Go app (SDK
|
|
315
|
+
* Packages a registry item may depend on that work in the Expo Go app (SDK 57): native modules Expo Go
|
|
316
316
|
* ships, plus pure-JS packages. Anything else needs a development build (`npx expo run:ios|android`).
|
|
317
317
|
* Keep in sync with the Expo Go module list when the SDK moves.
|
|
318
318
|
*/
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the liquidglass-tab template (no I/O): which Expo Router API shape to write for
|
|
3
|
+
* the installed SDK, starter-app detection, and tabs.ts generation for an existing app.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { CliError } from './lib.mjs';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Expo SDK → API shape
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/** Newest SDK this CLI knows the API of; also assumed when neither expo nor expo-router is installed. */
|
|
13
|
+
export const LATEST_KNOWN_SDK = 58;
|
|
14
|
+
export const MIN_SDK = 54;
|
|
15
|
+
|
|
16
|
+
/** "~6.0.24", "^54.0.0", "6.0.24", "54" → [6, 0, 24]; "latest", "*", "file:…" → null. */
|
|
17
|
+
export function parseVersion(spec) {
|
|
18
|
+
const m = /^(?:[\^~]|>=?|=)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(String(spec ?? '').trim());
|
|
19
|
+
return m ? [Number(m[1]), Number(m[2] ?? 0), Number(m[3] ?? 0)] : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** expo-router majors before it switched to SDK numbering (55.x = SDK 55). */
|
|
23
|
+
const ROUTER_TO_SDK = { 6: 54, 5: 53, 4: 52, 3: 50, 2: 49 };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* What to write for an installed expo / expo-router (versions or package.json ranges):
|
|
27
|
+
* SDK 54 NativeTabs + standalone Icon/Label/Badge/VectorIcon; Tabs from 'expo-router'
|
|
28
|
+
* SDK 55 NativeTabs.Trigger.*; Tabs from 'expo-router'
|
|
29
|
+
* SDK 56–57 NativeTabs.Trigger.*; Tabs from 'expo-router/js-tabs'; themes from 'expo-router'
|
|
30
|
+
* SDK 58+ as 56–57, NativeTabs from 'expo-router/native-tabs'
|
|
31
|
+
*/
|
|
32
|
+
export function routerTarget({ router, expo } = {}) {
|
|
33
|
+
const r = parseVersion(router);
|
|
34
|
+
const e = parseVersion(expo);
|
|
35
|
+
let sdk = null;
|
|
36
|
+
let source = null;
|
|
37
|
+
if (r) {
|
|
38
|
+
sdk = r[0] >= 50 ? r[0] : (ROUTER_TO_SDK[r[0]] ?? (r[0] < 2 ? 48 : null));
|
|
39
|
+
source = `expo-router ${r.join('.')}`;
|
|
40
|
+
}
|
|
41
|
+
if (sdk === null && e) {
|
|
42
|
+
sdk = e[0];
|
|
43
|
+
source = `expo ${e.join('.')}`;
|
|
44
|
+
}
|
|
45
|
+
const assumed = sdk === null;
|
|
46
|
+
if (assumed) sdk = LATEST_KNOWN_SDK;
|
|
47
|
+
if (sdk < MIN_SDK) return { sdk, source, unsupported: true };
|
|
48
|
+
const legacyNav = sdk <= 55;
|
|
49
|
+
return {
|
|
50
|
+
sdk,
|
|
51
|
+
source: source ?? `assumed SDK ${sdk}`,
|
|
52
|
+
assumed,
|
|
53
|
+
icons: sdk === 54 ? 'standalone' : 'trigger',
|
|
54
|
+
nativeTabs: sdk >= 58 ? 'expo-router/native-tabs' : 'expo-router/unstable-native-tabs',
|
|
55
|
+
tabs: legacyNav ? 'expo-router' : 'expo-router/js-tabs',
|
|
56
|
+
tabBarProps: legacyNav ? '@react-navigation/bottom-tabs' : 'expo-router/js-tabs',
|
|
57
|
+
themes: legacyNav ? '@react-navigation/native' : 'expo-router',
|
|
58
|
+
/** Imported directly on SDK 54–55 (expo-router depends on them; strict installers need them listed). */
|
|
59
|
+
deps: legacyNav ? ['@react-navigation/bottom-tabs', '@react-navigation/native'] : [],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function replaceOnce(text, re, replacement, what) {
|
|
64
|
+
if (!re.test(text)) throw new CliError(`liquidglass-tab template changed shape (${what}) — update the CLI: npx @expokit/cli@latest`);
|
|
65
|
+
return text.replace(re, replacement);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Registry `src/`-relative paths in comments ("src/app/(tabs)", "src/lib/tabs.ts") → the project's folders. */
|
|
69
|
+
export function localizePaths(text, paths = {}) {
|
|
70
|
+
const map = { app: paths.app, components: paths.components, lib: paths.lib };
|
|
71
|
+
return text.replace(/\bsrc\/(app|components|lib)\//g, (all, folder) => (map[folder] ? `${map[folder].replace(/\/+$/, '')}/` : all));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A template file rewritten for `target` (from routerTarget). Files without SDK-specific imports come back unchanged. */
|
|
75
|
+
export function adaptLiquidGlassFile(rel, text, target) {
|
|
76
|
+
if (rel === 'components/LiquidGlassTabs.tsx') {
|
|
77
|
+
const standalone = target.icons === 'standalone';
|
|
78
|
+
const header =
|
|
79
|
+
`// Expo SDK ${target.sdk}${standalone ? ': standalone Icon / Label / Badge / VectorIcon (NativeTabs.Trigger.* is SDK 55+)' : ''}.\n` +
|
|
80
|
+
`// After upgrading Expo, run \`npx @expokit/cli@latest update\` to get the matching imports.\n` +
|
|
81
|
+
`import { Tabs } from '${target.tabs}';\n` +
|
|
82
|
+
(standalone
|
|
83
|
+
? `import { Badge, Icon, Label, NativeTabs, VectorIcon } from '${target.nativeTabs}';\n`
|
|
84
|
+
: `import { NativeTabs } from '${target.nativeTabs}';\n`);
|
|
85
|
+
let out = replaceOnce(
|
|
86
|
+
text,
|
|
87
|
+
/\/\/ Written for your Expo SDK[^\n]*\n\/\/[^\n]*\nimport \{ Tabs \} from 'expo-router\/js-tabs';\nimport \{ NativeTabs \} from 'expo-router\/unstable-native-tabs';\n/,
|
|
88
|
+
header,
|
|
89
|
+
'LiquidGlassTabs imports'
|
|
90
|
+
);
|
|
91
|
+
if (standalone) out = out.replace(/\bNativeTabs\.Trigger\.(Icon|Label|Badge|VectorIcon)\b/g, '$1');
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
if (rel === 'components/GlassTabBar.tsx') {
|
|
95
|
+
return replaceOnce(
|
|
96
|
+
text,
|
|
97
|
+
/\/\/ '@react-navigation\/bottom-tabs' on SDK[^\n]*\nimport type \{ BottomTabBarProps \} from 'expo-router\/js-tabs';\n/,
|
|
98
|
+
`import type { BottomTabBarProps } from '${target.tabBarProps}';\n`,
|
|
99
|
+
'GlassTabBar imports'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (rel === 'app/_layout.tsx') {
|
|
103
|
+
const imports =
|
|
104
|
+
target.themes === 'expo-router'
|
|
105
|
+
? "// SDK 56+: navigation themes come from expo-router itself. Using @react-navigation/native's\n" +
|
|
106
|
+
'// ThemeProvider here does nothing (expo-router ships its own copy) and causes a white flash.\n' +
|
|
107
|
+
"import { DarkTheme, DefaultTheme, Slot, ThemeProvider } from 'expo-router';\n"
|
|
108
|
+
: "import { Slot } from 'expo-router';\n" +
|
|
109
|
+
`// SDK ${target.sdk}: navigation themes come from React Navigation (expo-router re-exports them from SDK 56).\n` +
|
|
110
|
+
"import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';\n";
|
|
111
|
+
return replaceOnce(
|
|
112
|
+
text,
|
|
113
|
+
/\/\/ Navigation themes:[^\n]*\n\/\/[^\n]*\nimport \{ DarkTheme, DefaultTheme, Slot, ThemeProvider \} from 'expo-router';\n/,
|
|
114
|
+
imports,
|
|
115
|
+
'root layout imports'
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return text;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Existing apps: routes → tabs.ts
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
const ROUTE_EXT = /\.(tsx|ts|jsx|js)$/;
|
|
126
|
+
const PLATFORM_EXT = /\.(ios|android|web|native)$/;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Tab names of a navigator from the entries of its folder (`{ name, dir }`): route files minus
|
|
130
|
+
* platform suffixes, and sub-folders. Skips layouts, `+not-found`-style special files and `_`/`.` names.
|
|
131
|
+
*/
|
|
132
|
+
export function routeNamesFromEntries(entries) {
|
|
133
|
+
const names = [];
|
|
134
|
+
for (const { name, dir } of entries) {
|
|
135
|
+
if (name.startsWith('.') || name.startsWith('_') || name.startsWith('+')) continue;
|
|
136
|
+
let route;
|
|
137
|
+
if (dir) route = name;
|
|
138
|
+
else {
|
|
139
|
+
if (!ROUTE_EXT.test(name) || name.endsWith('.d.ts')) continue;
|
|
140
|
+
route = name.replace(ROUTE_EXT, '').replace(PLATFORM_EXT, '');
|
|
141
|
+
}
|
|
142
|
+
if (!names.includes(route)) names.push(route);
|
|
143
|
+
}
|
|
144
|
+
return names;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** "index" → "Home", "my-cards" → "My cards", "[id]" → "Id". */
|
|
148
|
+
export function tabTitle(name) {
|
|
149
|
+
if (name === 'index') return 'Home';
|
|
150
|
+
const words = String(name).replace(/[[\]().]/g, ' ').split(/[\s_-]+/).filter(Boolean);
|
|
151
|
+
if (!words.length) return String(name);
|
|
152
|
+
const text = words.join(' ').toLowerCase();
|
|
153
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Tab order and titles from an existing layout: `<Tabs.Screen name="x" options={{ title: 'X' }}>` or
|
|
158
|
+
* `<NativeTabs.Trigger name="x"><…Label>X</…Label>`. Best effort — unknown shapes give [].
|
|
159
|
+
*/
|
|
160
|
+
export function readTabsFromLayout(text) {
|
|
161
|
+
const src = String(text ?? '');
|
|
162
|
+
const re = /<(Tabs\.Screen|NativeTabs\.Trigger)\b[^>]*?\bname=["'{]+([^"'}]+)["'}]+/g;
|
|
163
|
+
const hits = [...src.matchAll(re)];
|
|
164
|
+
return hits.map((m, i) => {
|
|
165
|
+
const end = i + 1 < hits.length ? hits[i + 1].index : src.length;
|
|
166
|
+
const block = src.slice(m.index, end);
|
|
167
|
+
const title =
|
|
168
|
+
/\btitle:\s*["']([^"']+)["']/.exec(block)?.[1] ??
|
|
169
|
+
/<(?:NativeTabs\.Trigger\.)?Label\b[^>]*>\s*([^<{]+?)\s*<\//.exec(block)?.[1] ??
|
|
170
|
+
null;
|
|
171
|
+
return { name: m[2], title };
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** [match words, SF Symbol default/selected, Ionicons default/selected] — a sensible icon per common tab name. */
|
|
176
|
+
const ICON_GUESSES = [
|
|
177
|
+
[['index', 'home', 'feed'], 'house', 'house.fill', 'home-outline', 'home'],
|
|
178
|
+
[['explore', 'discover', 'browse'], 'safari', 'safari.fill', 'compass-outline', 'compass'],
|
|
179
|
+
[['search', 'find'], 'magnifyingglass', 'magnifyingglass', 'search-outline', 'search'],
|
|
180
|
+
[['profile', 'account', 'me', 'you', 'user'], 'person.crop.circle', 'person.crop.circle.fill', 'person-circle-outline', 'person-circle'],
|
|
181
|
+
[['settings', 'preferences'], 'gearshape', 'gearshape.fill', 'settings-outline', 'settings'],
|
|
182
|
+
[['alerts', 'notifications', 'activity'], 'bell', 'bell.fill', 'notifications-outline', 'notifications'],
|
|
183
|
+
[['messages', 'chat', 'chats', 'inbox'], 'bubble.left.and.bubble.right', 'bubble.left.and.bubble.right.fill', 'chatbubbles-outline', 'chatbubbles'],
|
|
184
|
+
[['cart', 'bag', 'shop', 'store', 'orders'], 'bag', 'bag.fill', 'bag-outline', 'bag'],
|
|
185
|
+
[['favorites', 'favourites', 'likes', 'saved'], 'heart', 'heart.fill', 'heart-outline', 'heart'],
|
|
186
|
+
[['cards', 'wallet', 'pay', 'payments'], 'creditcard', 'creditcard.fill', 'card-outline', 'card'],
|
|
187
|
+
[['calendar', 'schedule', 'events'], 'calendar', 'calendar', 'calendar-outline', 'calendar'],
|
|
188
|
+
[['library', 'books'], 'books.vertical', 'books.vertical.fill', 'library-outline', 'library'],
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
export function guessTabIcon(name) {
|
|
192
|
+
const key = String(name).toLowerCase().replace(/[^a-z]/g, '');
|
|
193
|
+
const hit = ICON_GUESSES.find(([words]) => words.includes(key)) ?? ICON_GUESSES.find(([words]) => words.some((w) => key.includes(w)));
|
|
194
|
+
const [, sf, sfSelected, ion, ionSelected] = hit ?? [null, 'circle', 'circle.fill', 'ellipse-outline', 'ellipse'];
|
|
195
|
+
return { sf, sfSelected, ion, ionSelected };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const q = (s) => `'${String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
199
|
+
|
|
200
|
+
/** One TABS entry (same layout as the template's, so parseTabNamesFromConfig reads it back). */
|
|
201
|
+
export function tabEntrySource({ name, title }) {
|
|
202
|
+
const icon = guessTabIcon(name);
|
|
203
|
+
const sf = icon.sf === icon.sfSelected ? q(icon.sf) : `{ default: ${q(icon.sf)}, selected: ${q(icon.sfSelected)} }`;
|
|
204
|
+
const lines = [
|
|
205
|
+
' {',
|
|
206
|
+
` name: ${q(name)},`,
|
|
207
|
+
` title: ${q(title ?? tabTitle(name))},`,
|
|
208
|
+
...(/^\[.*\]$/.test(name) ? [' hidden: true,'] : []),
|
|
209
|
+
` sf: ${sf},`,
|
|
210
|
+
` icon: { set: 'Ionicons', name: ${q(icon.ion)}, selected: ${q(icon.ionSelected)} },`,
|
|
211
|
+
' },',
|
|
212
|
+
];
|
|
213
|
+
return lines.join('\n');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Tabs for an existing app: its route names, ordered and titled like its current layout when that
|
|
218
|
+
* layout names them, `index` first otherwise.
|
|
219
|
+
*/
|
|
220
|
+
export function tabsFromRoutes(routes, layoutTabs = []) {
|
|
221
|
+
const known = layoutTabs.filter((t) => routes.includes(t.name));
|
|
222
|
+
const rest = routes.filter((r) => !known.some((t) => t.name === r)).sort((a, b) => (a === 'index' ? -1 : b === 'index' ? 1 : a.localeCompare(b)));
|
|
223
|
+
return [...known.map((t) => ({ name: t.name, title: t.title ?? tabTitle(t.name) })), ...rest.map((name) => ({ name, title: tabTitle(name) }))];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The template's tabs.ts with its TABS list replaced by `tabs`. */
|
|
227
|
+
export function replaceTabsList(tabsTs, tabs) {
|
|
228
|
+
const body = tabs.map(tabEntrySource).join('\n');
|
|
229
|
+
return replaceOnce(tabsTs, /export const TABS: TabItem\[\] = \[[\s\S]*?\n\];/, `export const TABS: TabItem[] = [\n${body}\n];`, 'tabs.ts TABS');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// create-expo-app starters (safe to replace: nothing of the user's in them yet)
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
/** SDK 54 starter app/(tabs)/_layout.tsx: JS Tabs with HapticTab + IconSymbol. */
|
|
237
|
+
export function isStarterTabsLayout(text) {
|
|
238
|
+
const s = String(text ?? '');
|
|
239
|
+
return /from ['"]@\/components\/haptic-?tab['"]/i.test(s) && /IconSymbol/.test(s) && !/Redirect|useAuth|useSession/.test(s);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** SDK 55+ starter components/app-tabs(.web).tsx: the starter's two tabs (index, explore). */
|
|
243
|
+
export function isStarterAppTabs(text) {
|
|
244
|
+
const s = String(text ?? '');
|
|
245
|
+
if (/Redirect|useAuth|useSession/.test(s)) return false;
|
|
246
|
+
const native = /tabIcons\/home\.png/.test(s) && /tabIcons\/explore\.png/.test(s);
|
|
247
|
+
const web = /from ['"]expo-router\/ui['"]/.test(s) && /TabList/.test(s) && /href=["']\/explore["']/.test(s);
|
|
248
|
+
return native || web;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** create-expo-app root layouts (SDK 54: Stack anchored on (tabs) + modal; SDK 55+: AnimatedSplashOverlay + AppTabs). */
|
|
252
|
+
export function isStarterRootLayout(text) {
|
|
253
|
+
const s = String(text ?? '');
|
|
254
|
+
const sdk54 = /anchor:\s*['"]\(tabs\)['"]/.test(s) && /name=["']modal["']/.test(s);
|
|
255
|
+
const sdk55 = /AnimatedSplashOverlay/.test(s) && /AppTabs/.test(s);
|
|
256
|
+
const providers = [...s.matchAll(/<(\w+Provider)\b/g)].map((m) => m[1]).filter((p) => p !== 'ThemeProvider');
|
|
257
|
+
return (sdk54 || sdk55) && !providers.length && !/Redirect/.test(s);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Files the template ships that a later `update` may refresh: anything that names its building blocks. */
|
|
261
|
+
export function isLiquidGlassSource(text) {
|
|
262
|
+
return /\b(USE_NATIVE_TABS|LiquidGlassTabs|GlassTabBar|TabScreenFromConfig|tabBarCollapse|asFamily|TAB_BAR)\b/.test(String(text ?? ''));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// package.json diff (what `npx expo install` changed)
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
/** `{ added: [[name, version]], changed: [[name, from, to]] }` between two package.json objects. */
|
|
270
|
+
export function diffDependencies(before = {}, after = {}) {
|
|
271
|
+
const all = (pkg) => ({ ...pkg?.devDependencies, ...pkg?.dependencies });
|
|
272
|
+
const a = all(before);
|
|
273
|
+
const b = all(after);
|
|
274
|
+
const added = [];
|
|
275
|
+
const changed = [];
|
|
276
|
+
for (const [name, version] of Object.entries(b)) {
|
|
277
|
+
if (!(name in a)) added.push([name, version]);
|
|
278
|
+
else if (a[name] !== version) changed.push([name, a[name], version]);
|
|
279
|
+
}
|
|
280
|
+
return { added, changed };
|
|
281
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expokit/cli",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"mcpName": "io.github.Anshkaran7/expokit",
|
|
5
|
+
"description": "Animated UI components for Expo and React Native — a shadcn-style CLI that copies buttons, inputs, cards, charts and full screens into your app",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"bin": {
|
|
@@ -17,9 +18,22 @@
|
|
|
17
18
|
"keywords": [
|
|
18
19
|
"expo",
|
|
19
20
|
"react-native",
|
|
20
|
-
"
|
|
21
|
+
"react-native-ui",
|
|
22
|
+
"ui-components",
|
|
23
|
+
"components",
|
|
24
|
+
"component-library",
|
|
25
|
+
"ui-kit",
|
|
21
26
|
"ui",
|
|
27
|
+
"design-system",
|
|
28
|
+
"animation",
|
|
29
|
+
"animated",
|
|
30
|
+
"haptics",
|
|
31
|
+
"dark-mode",
|
|
32
|
+
"ios",
|
|
33
|
+
"android",
|
|
22
34
|
"shadcn",
|
|
35
|
+
"shadcn-ui",
|
|
36
|
+
"cli",
|
|
23
37
|
"liquid-glass",
|
|
24
38
|
"native-tabs",
|
|
25
39
|
"expo-router"
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
{
|
|
105
105
|
"name": "SeparatedCards",
|
|
106
106
|
"title": "Separated Cards",
|
|
107
|
-
"code": "import { Text, View } from 'react-native';\nimport { Accordion, AccordionItem } from '@/components/ui/accordion';\n\nexport default function SeparatedCards() {\n return (\n <Accordion variant=\"separated\">\n {[\n ['🔒', 'Is my data encrypted?', 'Everything is encrypted in transit with TLS 1.3 and at rest with AES-256. Keys rotate every 90 days.'],\n ['💳', 'Do you offer refunds?', 'If ExpoKit is not for you, email us within 14 days of purchase and we will refund you in full.'],\n ['🌍', 'Which regions do you host in?', 'Frankfurt, Virginia and Singapore. Pick a region when you create a project.'],\n ].map(([icon, q, a]) => (\n <AccordionItem key={q} value={q} title={q} icon={<Text style={{ fontSize: 18 }}>{icon}</Text>}>\n {a}\n </AccordionItem>\n ))}\n </Accordion>\n );\n}\n"
|
|
107
|
+
"code": "import { Text, View } from 'react-native';\nimport { Accordion, AccordionItem } from '@/components/ui/accordion';\n\n/** Controls stop at iOS's largest standard text size; content text stays uncapped. */\nconst FONT_CAP = 1.35;\n\nexport default function SeparatedCards() {\n return (\n <Accordion variant=\"separated\">\n {[\n ['🔒', 'Is my data encrypted?', 'Everything is encrypted in transit with TLS 1.3 and at rest with AES-256. Keys rotate every 90 days.'],\n ['💳', 'Do you offer refunds?', 'If ExpoKit is not for you, email us within 14 days of purchase and we will refund you in full.'],\n ['🌍', 'Which regions do you host in?', 'Frankfurt, Virginia and Singapore. Pick a region when you create a project.'],\n ].map(([icon, q, a]) => (\n <AccordionItem key={q} value={q} title={q} icon={<Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 18 }}>{icon}</Text>}>\n {a}\n </AccordionItem>\n ))}\n </Accordion>\n );\n}\n"
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
110
|
"name": "Controlled",
|
|
@@ -186,22 +186,22 @@
|
|
|
186
186
|
{
|
|
187
187
|
"name": "ProfileHeader",
|
|
188
188
|
"title": "Profile Header",
|
|
189
|
-
"code": "import { Pressable, ScrollView, Text, View } from 'react-native';\nimport { Avatar, AvatarGroup } from '@/components/ui/avatar';\nimport { alpha, useKit } from '@/lib/kit';\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\nexport default function ProfileHeader({ screen }: DemoProps) {\n const { colors, radius, dark } = useKit();\n return (\n <View\n style={[\n { alignItems: 'center', gap: 12, paddingVertical: 20, paddingHorizontal: 16 },\n screen\n ? { flex: 1, justifyContent: 'center', backgroundColor: colors.background }\n : { borderRadius: radius.xl, backgroundColor: colors.card, borderWidth: 1, borderColor: alpha(colors.foreground, dark ? 0.1 : 0.06) },\n ]}\n >\n <Avatar\n size={88}\n src=\"https://i.pravatar.cc/240?img=47\"\n name=\"Ava Rossi\"\n ring=\"story\"\n verified\n surface={screen ? colors.background : colors.card}\n elevated\n />\n <View style={{ alignItems: 'center', gap: 2 }}>\n <Text style={{ fontSize: 19, fontWeight: '400', letterSpacing: -0.3, color: colors.foreground }}>Ava Rossi</Text>\n <Text style={{ fontSize: 14, color: colors.mutedForeground }}>Product designer · Milan</Text>\n </View>\n <View style={{ flexDirection: 'row', gap: 24, marginTop: 4 }}>\n {[\n ['248', 'Posts'],\n ['12.4k', 'Followers'],\n ['311', 'Following'],\n ].map(([n, l]) => (\n <View key={l} style={{ alignItems: 'center' }}>\n <Text style={{ fontSize: 16, color: colors.foreground, fontVariant: ['tabular-nums'] }}>{n}</Text>\n <Text style={{ fontSize: 12, color: colors.mutedForeground }}>{l}</Text>\n </View>\n ))}\n </View>\n </View>\n );\n}\n"
|
|
189
|
+
"code": "import { Pressable, ScrollView, Text, View } from 'react-native';\nimport { Avatar, AvatarGroup } from '@/components/ui/avatar';\nimport { alpha, useKit } from '@/lib/kit';\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\nexport default function ProfileHeader({ screen }: DemoProps) {\n const { colors, radius, dark } = useKit();\n return (\n <View\n style={[\n { alignItems: 'center', gap: 12, paddingVertical: 20, paddingHorizontal: 16 },\n screen\n ? { flex: 1, justifyContent: 'center', backgroundColor: colors.background }\n : { borderRadius: radius.xl, backgroundColor: colors.card, borderWidth: 1, borderColor: alpha(colors.foreground, dark ? 0.1 : 0.06) },\n ]}\n >\n <Avatar\n size={88}\n src=\"https://i.pravatar.cc/240?img=47\"\n name=\"Ava Rossi\"\n ring=\"story\"\n verified\n surface={screen ? colors.background : colors.card}\n elevated\n />\n <View style={{ alignItems: 'center', gap: 2 }}>\n <Text style={{ fontSize: 19, fontWeight: '400', letterSpacing: -0.3, color: colors.foreground }}>Ava Rossi</Text>\n <Text style={{ fontSize: 14, color: colors.mutedForeground }}>Product designer · Milan</Text>\n </View>\n <View style={{ flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center', gap: 24, marginTop: 4 }}>\n {[\n ['248', 'Posts'],\n ['12.4k', 'Followers'],\n ['311', 'Following'],\n ].map(([n, l]) => (\n <View key={l} style={{ alignItems: 'center' }}>\n <Text style={{ fontSize: 16, color: colors.foreground, fontVariant: ['tabular-nums'] }}>{n}</Text>\n <Text style={{ fontSize: 12, color: colors.mutedForeground }}>{l}</Text>\n </View>\n ))}\n </View>\n </View>\n );\n}\n"
|
|
190
190
|
},
|
|
191
191
|
{
|
|
192
192
|
"name": "ChatList",
|
|
193
193
|
"title": "Chat List",
|
|
194
|
-
"code": "import { Pressable, ScrollView, Text, View } from 'react-native';\nimport { Avatar, AvatarGroup } from '@/components/ui/avatar';\nimport { alpha, useKit } from '@/lib/kit';\n\nexport default function ChatList() {\n const { colors } = useKit();\n const chats = [\n { name: 'Noah Kim', src: 'https://i.pravatar.cc/150?img=59', status: 'online', msg: 'Pushed the fix — can you re-test?', time: '9:41', unread: 2 },\n { name: 'Priya Patel', status: 'away', msg: 'Sounds good, see you at 3.', time: '9:12', unread: 0 },\n { name: 'Diego Alvarez', src: 'https://i.pravatar.cc/150?img=12', status: 'busy', msg: 'In a call until noon', time: 'Mon', unread: 0 },\n { name: 'Hannah Lee', status: 'offline', msg: 'Thanks for the notes!', time: 'Sun', unread: 0 },\n ] as const;\n return (\n <View>\n {chats.map((c, i) => (\n <View\n key={c.name}\n style={{\n flexDirection: 'row',\n alignItems: 'center',\n gap: 12,\n paddingVertical: 10,\n borderTopWidth: i ? 1 : 0,\n borderTopColor: alpha(colors.foreground, 0.06),\n }}\n >\n <Avatar size={48} name={c.name} src={'src' in c ? c.src : undefined} status={c.status} />\n <View style={{ flex: 1, minWidth: 0 }}>\n <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>\n <Text style={{ fontSize: 15, color: colors.foreground }}>{c.name}</Text>\n <Text style={{ fontSize: 12, color: c.unread ? colors.accent : colors.mutedForeground }}>{c.time}</Text>\n </View>\n <Text numberOfLines={1} style={{ fontSize: 14, color: colors.mutedForeground, marginTop: 2 }}>\n {c.msg}\n </Text>\n </View>\n </View>\n ))}\n </View>\n );\n}\n"
|
|
194
|
+
"code": "import { Pressable, ScrollView, Text, View } from 'react-native';\nimport { Avatar, AvatarGroup } from '@/components/ui/avatar';\nimport { alpha, useKit } from '@/lib/kit';\n\nexport default function ChatList() {\n const { colors } = useKit();\n const chats = [\n { name: 'Noah Kim', src: 'https://i.pravatar.cc/150?img=59', status: 'online', msg: 'Pushed the fix — can you re-test?', time: '9:41', unread: 2 },\n { name: 'Priya Patel', status: 'away', msg: 'Sounds good, see you at 3.', time: '9:12', unread: 0 },\n { name: 'Diego Alvarez', src: 'https://i.pravatar.cc/150?img=12', status: 'busy', msg: 'In a call until noon', time: 'Mon', unread: 0 },\n { name: 'Hannah Lee', status: 'offline', msg: 'Thanks for the notes!', time: 'Sun', unread: 0 },\n ] as const;\n return (\n <View>\n {chats.map((c, i) => (\n <View\n key={c.name}\n style={{\n flexDirection: 'row',\n alignItems: 'center',\n gap: 12,\n paddingVertical: 10,\n borderTopWidth: i ? 1 : 0,\n borderTopColor: alpha(colors.foreground, 0.06),\n }}\n >\n <Avatar size={48} name={c.name} src={'src' in c ? c.src : undefined} status={c.status} />\n <View style={{ flex: 1, minWidth: 0 }}>\n <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>\n <Text style={{ flexShrink: 1, fontSize: 15, color: colors.foreground }}>{c.name}</Text>\n <Text style={{ fontSize: 12, color: c.unread ? colors.accent : colors.mutedForeground }}>{c.time}</Text>\n </View>\n <Text numberOfLines={1} style={{ fontSize: 14, color: colors.mutedForeground, marginTop: 2 }}>\n {c.msg}\n </Text>\n </View>\n </View>\n ))}\n </View>\n );\n}\n"
|
|
195
195
|
},
|
|
196
196
|
{
|
|
197
197
|
"name": "Stories",
|
|
198
198
|
"title": "Stories",
|
|
199
|
-
"code": "import React, { useState } from 'react';\nimport { Pressable, ScrollView, Text, View } from 'react-native';\nimport { Avatar, AvatarGroup } from '@/components/ui/avatar';\nimport { alpha, useKit } from '@/lib/kit';\n\nexport default function Stories() {\n const { colors } = useKit();\n const [seen, setSeen] = useState<string[]>(['Theo']);\n const people = [\n { name: 'Kai Müller', short: 'Kai', img: 11 },\n { name: 'Mila Novak', short: 'Mila', img: 25 },\n { name: 'Theo Grant', short: 'Theo', img: 33 },\n { name: 'Nora Ali', short: 'Nora', img: 41 },\n { name: 'Sam Ortiz', short: 'Sam', img: 0 },\n { name: 'Lucía Romero', short: 'Lucía', img: 44 },\n ];\n return (\n <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 14, paddingRight: 8 }}>\n {people.map((p, i) => (\n <View key={p.short} style={{ alignItems: 'center', gap: 6 }}>\n <Avatar\n size=\"lg\"\n src={p.img ? `https://i.pravatar.cc/150?img=${p.img}` : undefined}\n name={p.name}\n ring=\"story\"\n seen={seen.includes(p.short)}\n ringSpin={i === 1 && !seen.includes(p.short)}\n onPress={() => setSeen((s) => (s.includes(p.short) ? s.filter((n) => n !== p.short) : [...s, p.short]))}\n />\n <Text style={{ fontSize: 12, color: seen.includes(p.short) ? colors.mutedForeground : colors.foreground }}>{p.short}</Text>\n </View>\n ))}\n </ScrollView>\n );\n}\n"
|
|
199
|
+
"code": "import React, { useState } from 'react';\nimport { Pressable, ScrollView, Text, View } from 'react-native';\nimport { Avatar, AvatarGroup } from '@/components/ui/avatar';\nimport { alpha, useKit } from '@/lib/kit';\n\n/** Demo control text (buttons, chips, labels on fixed shapes) stops at iOS's largest standard text size. */\nconst FONT_CAP = 1.35;\n\nexport default function Stories() {\n const { colors } = useKit();\n const [seen, setSeen] = useState<string[]>(['Theo']);\n const people = [\n { name: 'Kai Müller', short: 'Kai', img: 11 },\n { name: 'Mila Novak', short: 'Mila', img: 25 },\n { name: 'Theo Grant', short: 'Theo', img: 33 },\n { name: 'Nora Ali', short: 'Nora', img: 41 },\n { name: 'Sam Ortiz', short: 'Sam', img: 0 },\n { name: 'Lucía Romero', short: 'Lucía', img: 44 },\n ];\n return (\n <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 14, paddingRight: 8 }}>\n {people.map((p, i) => (\n <View key={p.short} style={{ alignItems: 'center', gap: 6 }}>\n <Avatar\n size=\"lg\"\n src={p.img ? `https://i.pravatar.cc/150?img=${p.img}` : undefined}\n name={p.name}\n ring=\"story\"\n seen={seen.includes(p.short)}\n ringSpin={i === 1 && !seen.includes(p.short)}\n onPress={() => setSeen((s) => (s.includes(p.short) ? s.filter((n) => n !== p.short) : [...s, p.short]))}\n />\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 12, color: seen.includes(p.short) ? colors.mutedForeground : colors.foreground }}>{p.short}</Text>\n </View>\n ))}\n </ScrollView>\n );\n}\n"
|
|
200
200
|
},
|
|
201
201
|
{
|
|
202
202
|
"name": "Group",
|
|
203
203
|
"title": "Group",
|
|
204
|
-
"code": "import { Pressable, ScrollView, Text, View } from 'react-native';\nimport { Avatar, AvatarGroup } from '@/components/ui/avatar';\nimport { alpha, useKit } from '@/lib/kit';\n\nexport default function Group() {\n const { colors, radius, dark } = useKit();\n return (\n <View style={{ gap: 14 }}>\n <View\n style={{\n padding: 14,\n gap: 12,\n borderRadius: radius.lg,\n backgroundColor: colors.card,\n borderWidth: 1,\n borderColor: alpha(colors.foreground, dark ? 0.1 : 0.06),\n }}\n >\n <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>\n <View>\n <Text style={{ fontSize: 15, color: colors.foreground }}>Mobile launch</Text>\n <Text style={{ fontSize: 13, color: colors.mutedForeground }}>Design · Engineering</Text>\n </View>\n <AvatarGroup max={4} total={9} surface={colors.card} size=\"sm\" onPressMore={() => {}}>\n {[8, 16, 26, 36, 48].map((n, i) => (\n <Avatar key={n} src={`https://i.pravatar.cc/150?img=${n}`} name={['Maya Chen', 'Leo Park', 'Ava Rossi', 'Noah Kim', 'Zoe Martin'][i]} />\n ))}\n </AvatarGroup>\n </View>\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>\n <AvatarGroup size=\"sm\" max={3} total={1284}>\n <Avatar name=\"Ana Silva\" />\n <Avatar name=\"Ben Carter\" />\n <Avatar name=\"Chloe Dubois\" />\n </AvatarGroup>\n <Text style={{ fontSize: 13, color: colors.mutedForeground }}>1,284 members</Text>\n </View>\n <AvatarGroup size=\"lg\" shape=\"rounded\" max={5}>\n {['Priya Patel', 'Omar Haddad', 'Sven Larsen', 'Iris Okafor'].map((n) => (\n <Avatar key={n} name={n} />\n ))}\n </AvatarGroup>\n </View>\n );\n}\n"
|
|
204
|
+
"code": "import { Pressable, ScrollView, Text, View } from 'react-native';\nimport { Avatar, AvatarGroup } from '@/components/ui/avatar';\nimport { alpha, useKit } from '@/lib/kit';\n\nexport default function Group() {\n const { colors, radius, dark } = useKit();\n return (\n <View style={{ gap: 14 }}>\n <View\n style={{\n padding: 14,\n gap: 12,\n borderRadius: radius.lg,\n backgroundColor: colors.card,\n borderWidth: 1,\n borderColor: alpha(colors.foreground, dark ? 0.1 : 0.06),\n }}\n >\n <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>\n <View style={{ flexShrink: 1 }}>\n <Text style={{ fontSize: 15, color: colors.foreground }}>Mobile launch</Text>\n <Text style={{ fontSize: 13, color: colors.mutedForeground }}>Design · Engineering</Text>\n </View>\n <AvatarGroup max={4} total={9} surface={colors.card} size=\"sm\" onPressMore={() => {}}>\n {[8, 16, 26, 36, 48].map((n, i) => (\n <Avatar key={n} src={`https://i.pravatar.cc/150?img=${n}`} name={['Maya Chen', 'Leo Park', 'Ava Rossi', 'Noah Kim', 'Zoe Martin'][i]} />\n ))}\n </AvatarGroup>\n </View>\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>\n <AvatarGroup size=\"sm\" max={3} total={1284}>\n <Avatar name=\"Ana Silva\" />\n <Avatar name=\"Ben Carter\" />\n <Avatar name=\"Chloe Dubois\" />\n </AvatarGroup>\n <Text style={{ flexShrink: 1, fontSize: 13, color: colors.mutedForeground }}>1,284 members</Text>\n </View>\n <AvatarGroup size=\"lg\" shape=\"rounded\" max={5}>\n {['Priya Patel', 'Omar Haddad', 'Sven Larsen', 'Iris Okafor'].map((n) => (\n <Avatar key={n} name={n} />\n ))}\n </AvatarGroup>\n </View>\n );\n}\n"
|
|
205
205
|
},
|
|
206
206
|
{
|
|
207
207
|
"name": "Fallbacks",
|
|
@@ -216,12 +216,12 @@
|
|
|
216
216
|
{
|
|
217
217
|
"name": "Status",
|
|
218
218
|
"title": "Status",
|
|
219
|
-
"code": "import { Pressable, ScrollView, Text, View } from 'react-native';\nimport { Avatar, AvatarGroup } from '@/components/ui/avatar';\nimport { alpha, useKit } from '@/lib/kit';\n\nexport default function Status() {\n const { colors } = useKit();\n const rows = [\n { name: 'Ethan Brooks', img: 15, status: 'online' },\n { name: 'Lina Novak', img: 44, status: 'away' },\n { name: 'Marcus Hill', img: 68, status: 'busy' },\n { name: 'Iris Okafor', img: 0, status: 'offline' },\n ] as const;\n return (\n <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>\n {rows.map((r) => (\n <View key={r.name} style={{ alignItems: 'center', gap: 6 }}>\n <Avatar size=\"lg\" src={r.img ? `https://i.pravatar.cc/150?img=${r.img}` : undefined} name={r.name} status={r.status} />\n <Text style={{ fontSize: 12, color: colors.mutedForeground }}>{r.status[0].toUpperCase() + r.status.slice(1)}</Text>\n </View>\n ))}\n </View>\n );\n}\n"
|
|
219
|
+
"code": "import { Pressable, ScrollView, Text, View } from 'react-native';\nimport { Avatar, AvatarGroup } from '@/components/ui/avatar';\nimport { alpha, useKit } from '@/lib/kit';\n\n/** Demo control text (buttons, chips, labels on fixed shapes) stops at iOS's largest standard text size. */\nconst FONT_CAP = 1.35;\n\nexport default function Status() {\n const { colors } = useKit();\n const rows = [\n { name: 'Ethan Brooks', img: 15, status: 'online' },\n { name: 'Lina Novak', img: 44, status: 'away' },\n { name: 'Marcus Hill', img: 68, status: 'busy' },\n { name: 'Iris Okafor', img: 0, status: 'offline' },\n ] as const;\n return (\n <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>\n {rows.map((r) => (\n <View key={r.name} style={{ alignItems: 'center', gap: 6 }}>\n <Avatar size=\"lg\" src={r.img ? `https://i.pravatar.cc/150?img=${r.img}` : undefined} name={r.name} status={r.status} />\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 12, color: colors.mutedForeground }}>{r.status[0].toUpperCase() + r.status.slice(1)}</Text>\n </View>\n ))}\n </View>\n );\n}\n"
|
|
220
220
|
},
|
|
221
221
|
{
|
|
222
222
|
"name": "Badges",
|
|
223
223
|
"title": "Badges",
|
|
224
|
-
"code": "import React, { useState } from 'react';\nimport { Pressable, ScrollView, Text, View } from 'react-native';\nimport { Avatar, AvatarGroup } from '@/components/ui/avatar';\nimport { alpha, useKit } from '@/lib/kit';\n\nexport default function Badges() {\n const { colors } = useKit();\n const [count, setCount] = useState(3);\n return (\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 22 }}>\n <Pressable accessibilityRole=\"button\" accessibilityLabel=\"Add a notification\" onPress={() => setCount((c) => c + 1)} style={{ alignItems: 'center', gap: 6 }}>\n <Avatar size=\"lg\" src=\"https://i.pravatar.cc/150?img=22\" name=\"Maya Chen\" badge={count} />\n <Text style={{ fontSize: 12, color: colors.mutedForeground }}>Tap +1</Text>\n </Pressable>\n <View style={{ alignItems: 'center', gap: 6 }}>\n <Avatar size=\"lg\" src=\"https://i.pravatar.cc/150?img=9\" name=\"Leo Park\" badge={128} status=\"online\" />\n <Text style={{ fontSize: 12, color: colors.mutedForeground }}>Mentions</Text>\n </View>\n <View style={{ alignItems: 'center', gap: 6 }}>\n <Avatar size=\"lg\" name=\"Design Team\" shape=\"rounded\" badge verified />\n <Text style={{ fontSize: 12, color: colors.mutedForeground }}>Workspace</Text>\n </View>\n </View>\n );\n}\n"
|
|
224
|
+
"code": "import React, { useState } from 'react';\nimport { Pressable, ScrollView, Text, View } from 'react-native';\nimport { Avatar, AvatarGroup } from '@/components/ui/avatar';\nimport { alpha, useKit } from '@/lib/kit';\n\n/** Demo control text (buttons, chips, labels on fixed shapes) stops at iOS's largest standard text size. */\nconst FONT_CAP = 1.35;\n\nexport default function Badges() {\n const { colors } = useKit();\n const [count, setCount] = useState(3);\n return (\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 22 }}>\n <Pressable accessibilityRole=\"button\" accessibilityLabel=\"Add a notification\" onPress={() => setCount((c) => c + 1)} style={{ alignItems: 'center', gap: 6 }}>\n <Avatar size=\"lg\" src=\"https://i.pravatar.cc/150?img=22\" name=\"Maya Chen\" badge={count} />\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 12, color: colors.mutedForeground }}>Tap +1</Text>\n </Pressable>\n <View style={{ alignItems: 'center', gap: 6 }}>\n <Avatar size=\"lg\" src=\"https://i.pravatar.cc/150?img=9\" name=\"Leo Park\" badge={128} status=\"online\" />\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 12, color: colors.mutedForeground }}>Mentions</Text>\n </View>\n <View style={{ alignItems: 'center', gap: 6 }}>\n <Avatar size=\"lg\" name=\"Design Team\" shape=\"rounded\" badge verified />\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 12, color: colors.mutedForeground }}>Workspace</Text>\n </View>\n </View>\n );\n}\n"
|
|
225
225
|
}
|
|
226
226
|
]
|
|
227
227
|
}
|
|
@@ -132,17 +132,17 @@
|
|
|
132
132
|
{
|
|
133
133
|
"name": "LiveStatus",
|
|
134
134
|
"title": "Live Status",
|
|
135
|
-
"code": "import React, { useEffect, useState } from 'react';\nimport { Pressable, Text, View } from 'react-native';\nimport { Badge, BadgeIcon, BadgeIconTile, NotificationBadge, type BadgeIconName, type BadgeTone } from '@/components/ui/badge';\nimport { alpha, useKit } from '@/lib/kit';\n\nconst DEPLOY: { label: string; tone: BadgeTone; icon: BadgeIconName }[] = [\n { label: 'Queued', tone: 'neutral', icon: 'clock' },\n { label: 'Building', tone: 'warning', icon: 'refresh' },\n { label: 'Deployed', tone: 'success', icon: 'check' },\n { label: 'Failed', tone: 'danger', icon: 'alert' },\n];\n\nexport default function LiveStatus() {\n const { colors } = useKit();\n const [step, setStep] = useState(0);\n useEffect(() => {\n const id = setInterval(() => setStep((i) => (i + 1) % DEPLOY.length), 1800);\n return () => clearInterval(id);\n }, []);\n const { label, tone, icon } = DEPLOY[step];\n const tint = { neutral: colors.mutedForeground, accent: colors.accent, success: colors.success, warning: colors.warning, danger: colors.danger }[tone];\n return (\n <View style={{ gap: 12 }}>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>\n <Text style={{ fontSize: 15, color: colors.foreground }}>web-checkout #482</Text>\n <Badge tone={tone} icon={<BadgeIcon name={icon} color={tint} size={14} />}>\n {label}\n </Badge>\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>\n <Text style={{ fontSize: 15, color: colors.foreground }}>api-gateway #117</Text>\n <Badge variant=\"solid\" tone={tone} size=\"lg\">\n {label}\n </Badge>\n <Badge variant=\"dot\" tone={tone} pulse={tone === 'warning'}>\n {label}\n </Badge>\n </View>\n </View>\n );\n}\n"
|
|
135
|
+
"code": "import React, { useEffect, useState } from 'react';\nimport { Pressable, Text, View } from 'react-native';\nimport { Badge, BadgeIcon, BadgeIconTile, NotificationBadge, type BadgeIconName, type BadgeTone } from '@/components/ui/badge';\nimport { alpha, useKit } from '@/lib/kit';\n\nconst DEPLOY: { label: string; tone: BadgeTone; icon: BadgeIconName }[] = [\n { label: 'Queued', tone: 'neutral', icon: 'clock' },\n { label: 'Building', tone: 'warning', icon: 'refresh' },\n { label: 'Deployed', tone: 'success', icon: 'check' },\n { label: 'Failed', tone: 'danger', icon: 'alert' },\n];\n\nexport default function LiveStatus() {\n const { colors } = useKit();\n const [step, setStep] = useState(0);\n useEffect(() => {\n const id = setInterval(() => setStep((i) => (i + 1) % DEPLOY.length), 1800);\n return () => clearInterval(id);\n }, []);\n const { label, tone, icon } = DEPLOY[step];\n const tint = { neutral: colors.mutedForeground, accent: colors.accent, success: colors.success, warning: colors.warning, danger: colors.danger }[tone];\n return (\n <View style={{ gap: 12 }}>\n <View style={{ flexDirection: 'row', flexWrap: 'wrap', alignItems: 'center', gap: 10 }}>\n <Text style={{ flexShrink: 1, fontSize: 15, color: colors.foreground }}>web-checkout #482</Text>\n <Badge tone={tone} icon={<BadgeIcon name={icon} color={tint} size={14} />}>\n {label}\n </Badge>\n </View>\n <View style={{ flexDirection: 'row', flexWrap: 'wrap', alignItems: 'center', gap: 10 }}>\n <Text style={{ flexShrink: 1, fontSize: 15, color: colors.foreground }}>api-gateway #117</Text>\n <Badge variant=\"solid\" tone={tone} size=\"lg\">\n {label}\n </Badge>\n <Badge variant=\"dot\" tone={tone} pulse={tone === 'warning'}>\n {label}\n </Badge>\n </View>\n </View>\n );\n}\n"
|
|
136
136
|
},
|
|
137
137
|
{
|
|
138
138
|
"name": "Sizes",
|
|
139
139
|
"title": "Sizes",
|
|
140
|
-
"code": "import { Pressable, Text, View } from 'react-native';\nimport { Badge, BadgeIcon, BadgeIconTile, NotificationBadge, type BadgeIconName, type BadgeTone } from '@/components/ui/badge';\n\nexport default function Sizes() {\n return (\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>\n <Badge size=\"sm\" tone=\"accent\">Small</Badge>\n <Badge size=\"md\" tone=\"accent\">Medium</Badge>\n <Badge size=\"lg\" tone=\"accent\">Large</Badge>\n </View>\n );\n}\n"
|
|
140
|
+
"code": "import { Pressable, Text, View } from 'react-native';\nimport { Badge, BadgeIcon, BadgeIconTile, NotificationBadge, type BadgeIconName, type BadgeTone } from '@/components/ui/badge';\n\nexport default function Sizes() {\n return (\n <View style={{ flexDirection: 'row', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>\n <Badge size=\"sm\" tone=\"accent\">Small</Badge>\n <Badge size=\"md\" tone=\"accent\">Medium</Badge>\n <Badge size=\"lg\" tone=\"accent\">Large</Badge>\n </View>\n );\n}\n"
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
143
|
"name": "Notifications",
|
|
144
144
|
"title": "Notifications",
|
|
145
|
-
"code": "import React, { useEffect, useState } from 'react';\nimport { Pressable, Text, View } from 'react-native';\nimport { Badge, BadgeIcon, BadgeIconTile, NotificationBadge, type BadgeIconName, type BadgeTone } from '@/components/ui/badge';\nimport { alpha, useKit } from '@/lib/kit';\n\nexport default function Notifications() {\n const { colors, radius } = useKit();\n const [count, setCount] = useState(3);\n return (\n <View style={{ gap: 18 }}>\n <View style={{ flexDirection: 'row', gap: 26 }}>\n <NotificationBadge count={count}>\n <BadgeIconTile name=\"bell\" backgroundColor={alpha(colors.accent, 0.12)} color={colors.accent} />\n </NotificationBadge>\n <NotificationBadge count={count * 37} tone=\"accent\">\n <BadgeIconTile name=\"message\" backgroundColor={alpha(colors.accent, 0.1)} color={colors.accent} />\n </NotificationBadge>\n <NotificationBadge count={count} dot tone=\"success\">\n <BadgeIconTile name=\"package\" backgroundColor={alpha(colors.success, 0.12)} color={colors.success} />\n </NotificationBadge>\n </View>\n <View style={{ flexDirection: 'row', gap: 10 }}>\n {[\n ['+ New', () => setCount((c) => c + 1)],\n ['Mark all read', () => setCount(0)],\n ].map(([label, onPress]) => (\n <Pressable\n key={label as string}\n onPress={onPress as () => void}\n style={{ paddingHorizontal: 14,
|
|
145
|
+
"code": "import React, { useEffect, useState } from 'react';\nimport { Pressable, Text, View } from 'react-native';\nimport { Badge, BadgeIcon, BadgeIconTile, NotificationBadge, type BadgeIconName, type BadgeTone } from '@/components/ui/badge';\nimport { alpha, useKit } from '@/lib/kit';\n\n/** Controls stop at iOS's largest standard text size; content text stays uncapped. */\nconst FONT_CAP = 1.35;\n\nexport default function Notifications() {\n const { colors, radius } = useKit();\n const [count, setCount] = useState(3);\n return (\n <View style={{ gap: 18 }}>\n <View style={{ flexDirection: 'row', gap: 26 }}>\n <NotificationBadge count={count}>\n <BadgeIconTile name=\"bell\" backgroundColor={alpha(colors.accent, 0.12)} color={colors.accent} />\n </NotificationBadge>\n <NotificationBadge count={count * 37} tone=\"accent\">\n <BadgeIconTile name=\"message\" backgroundColor={alpha(colors.accent, 0.1)} color={colors.accent} />\n </NotificationBadge>\n <NotificationBadge count={count} dot tone=\"success\">\n <BadgeIconTile name=\"package\" backgroundColor={alpha(colors.success, 0.12)} color={colors.success} />\n </NotificationBadge>\n </View>\n <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 10 }}>\n {[\n ['+ New', () => setCount((c) => c + 1)],\n ['Mark all read', () => setCount(0)],\n ].map(([label, onPress]) => (\n <Pressable\n key={label as string}\n onPress={onPress as () => void}\n style={{ paddingHorizontal: 14, minHeight: 36, borderRadius: radius.capsule(36), justifyContent: 'center', backgroundColor: colors.muted }}\n >\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontWeight: '400', color: colors.foreground }}>{label as string}</Text>\n </Pressable>\n ))}\n </View>\n </View>\n );\n}\n"
|
|
146
146
|
}
|
|
147
147
|
]
|
|
148
148
|
}
|