@expokit/cli 0.2.0 → 0.4.0
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 +20 -2
- package/bin/expo-kit.js +219 -44
- package/bin/lib.mjs +110 -1
- package/bin/mcp.mjs +48 -19
- package/package.json +9 -4
- package/registry/_docs/accordion.json +16 -0
- package/registry/_docs/action-sheet.json +28 -0
- package/registry/_docs/alert.json +28 -0
- package/registry/_docs/assistant-app.json +47 -0
- package/registry/_docs/avatar.json +25 -1
- package/registry/_docs/badge.json +36 -5
- package/registry/_docs/bottom-nav.json +30 -6
- package/registry/_docs/bottom-sheet.json +28 -0
- package/registry/_docs/bullet.json +3 -0
- package/registry/_docs/button-3d.json +18 -2
- package/registry/_docs/button.json +16 -0
- package/registry/_docs/calendar.json +21 -5
- package/registry/_docs/card-stack.json +16 -0
- package/registry/_docs/card.json +20 -0
- package/registry/_docs/carousel.json +20 -0
- package/registry/_docs/chat.json +184 -0
- package/registry/_docs/checkbox.json +16 -0
- package/registry/_docs/checkout.json +10 -1
- package/registry/_docs/chip-group.json +16 -0
- package/registry/_docs/confetti.json +23 -3
- package/registry/_docs/coupon.json +16 -0
- package/registry/_docs/cube-pager.json +16 -0
- package/registry/_docs/dashboard.json +3 -0
- package/registry/_docs/date-picker.json +24 -4
- package/registry/_docs/dialog.json +24 -0
- package/registry/_docs/donut-shop.json +88 -0
- package/registry/_docs/drawer.json +32 -4
- package/registry/_docs/fab.json +24 -0
- package/registry/_docs/fintech-app.json +49 -0
- package/registry/_docs/flip-card.json +16 -0
- package/registry/_docs/floating-input.json +25 -5
- package/registry/_docs/food-app.json +47 -0
- package/registry/_docs/gift-card.json +16 -0
- package/registry/_docs/glass-button.json +24 -0
- package/registry/_docs/glass-nav-bar.json +24 -0
- package/registry/_docs/glass-search-bar.json +24 -0
- package/registry/_docs/glass-segmented.json +28 -0
- package/registry/_docs/glass-sheet.json +28 -0
- package/registry/_docs/glass-tab-bar.json +24 -0
- package/registry/_docs/glass.json +17 -1
- package/registry/_docs/grain.json +80 -0
- package/registry/_docs/inbox.json +3 -0
- package/registry/_docs/isometric-bar.json +3 -0
- package/registry/_docs/kit.json +3 -0
- package/registry/_docs/liquidglass-tab.json +3 -0
- package/registry/_docs/list-item.json +16 -0
- package/registry/_docs/loyalty-card.json +16 -0
- package/registry/_docs/morphing-button-group.json +96 -0
- package/registry/_docs/number-ticker.json +16 -0
- package/registry/_docs/offers.json +3 -0
- package/registry/_docs/onboarding.json +3 -0
- package/registry/_docs/orb-reveal.json +132 -0
- package/registry/_docs/otp-input.json +28 -4
- package/registry/_docs/parallax-layers.json +20 -0
- package/registry/_docs/parallax-scroll.json +24 -0
- package/registry/_docs/password-input.json +23 -3
- package/registry/_docs/payment-card.json +16 -0
- package/registry/_docs/pixel-bar.json +3 -0
- package/registry/_docs/polar-area.json +3 -0
- package/registry/_docs/profile.json +3 -0
- package/registry/_docs/progress-steps.json +24 -0
- package/registry/_docs/progress.json +20 -0
- package/registry/_docs/qr-code-button.json +122 -0
- package/registry/_docs/radio-group.json +20 -0
- package/registry/_docs/rating.json +24 -0
- package/registry/_docs/ring-carousel.json +20 -0
- package/registry/_docs/scratch-card.json +20 -0
- package/registry/_docs/search-bar.json +23 -3
- package/registry/_docs/search-results.json +3 -0
- package/registry/_docs/segmented-control.json +20 -0
- package/registry/_docs/select.json +33 -8
- package/registry/_docs/settings.json +9 -0
- package/registry/_docs/sign-in.json +3 -0
- package/registry/_docs/skeleton.json +22 -2
- package/registry/_docs/slide-to-confirm.json +24 -0
- package/registry/_docs/slider.json +27 -7
- package/registry/_docs/spin-wheel.json +24 -0
- package/registry/_docs/spinner.json +41 -11
- package/registry/_docs/stateful-button.json +184 -0
- package/registry/_docs/stepper.json +23 -7
- package/registry/_docs/swipe-deck.json +16 -0
- package/registry/_docs/swipeable-row.json +24 -1
- package/registry/_docs/switch.json +20 -0
- package/registry/_docs/tabs.json +24 -0
- package/registry/_docs/text-animation.json +141 -0
- package/registry/_docs/textarea.json +18 -2
- package/registry/_docs/ticket-card.json +20 -0
- package/registry/_docs/ticket-shape.json +4 -1
- package/registry/_docs/tilt-card.json +20 -4
- package/registry/_docs/time-picker.json +22 -6
- package/registry/_docs/timeline-chart.json +3 -0
- package/registry/_docs/timeline.json +18 -2
- package/registry/_docs/toast.json +32 -8
- package/registry/_docs/treemap.json +3 -0
- package/registry/_docs/waterfall.json +3 -0
- package/registry/_docs/wheel-list.json +20 -0
- package/registry/accordion/files/src/components/ui/accordion.tsx +5 -4
- package/registry/accordion/files/src/lib/kit.tsx +8 -5
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +9 -8
- package/registry/action-sheet/files/src/lib/kit.tsx +8 -5
- package/registry/alert/files/src/components/ui/alert.tsx +7 -4
- package/registry/alert/files/src/components/ui/button.tsx +4 -3
- package/registry/alert/files/src/lib/kit.tsx +8 -5
- package/registry/assistant-app.json +49 -0
- package/registry/avatar/files/src/components/ui/avatar.tsx +7 -4
- package/registry/avatar/files/src/lib/kit.tsx +8 -5
- package/registry/badge/files/src/components/ui/badge.tsx +345 -21
- package/registry/badge/files/src/lib/kit.tsx +8 -5
- package/registry/badge.json +1 -1
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +14 -7
- package/registry/bottom-nav/files/src/lib/kit.tsx +8 -5
- package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +7 -6
- package/registry/bottom-sheet/files/src/lib/kit.tsx +8 -5
- package/registry/button/files/src/components/ui/button.tsx +4 -3
- package/registry/button/files/src/lib/kit.tsx +8 -5
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +3 -2
- package/registry/button-3d/files/src/lib/kit.tsx +8 -5
- package/registry/calendar/files/src/components/ui/calendar.tsx +9 -5
- package/registry/calendar/files/src/lib/kit.tsx +8 -5
- package/registry/card/files/src/components/ui/card.tsx +8 -6
- package/registry/card/files/src/lib/kit.tsx +8 -5
- package/registry/chat.json +18 -0
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +6 -5
- package/registry/checkbox/files/src/lib/kit.tsx +8 -5
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +4 -2
- package/registry/chip-group/files/src/lib/kit.tsx +8 -5
- package/registry/confetti/files/src/lib/kit.tsx +8 -5
- package/registry/coupon/files/src/components/ui/coupon.tsx +23 -20
- package/registry/coupon/files/src/components/ui/flip-card.tsx +1 -1
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +2 -2
- package/registry/coupon/files/src/lib/kit.tsx +8 -5
- package/registry/date-picker/files/src/components/ui/calendar.tsx +9 -5
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +2 -2
- package/registry/date-picker/files/src/lib/kit.tsx +8 -5
- package/registry/dialog/files/src/components/ui/dialog.tsx +12 -6
- package/registry/dialog/files/src/lib/kit.tsx +8 -5
- package/registry/donut-shop.json +18 -0
- package/registry/drawer/files/src/components/ui/drawer.tsx +25 -19
- package/registry/drawer/files/src/lib/kit.tsx +8 -5
- package/registry/fintech-app.json +57 -0
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +1 -1
- package/registry/flip-card/files/src/lib/kit.tsx +8 -5
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +16 -6
- package/registry/floating-input/files/src/lib/kit.tsx +8 -5
- package/registry/food-app.json +55 -0
- package/registry/grain/files/src/components/ui/grain.tsx +113 -0
- package/registry/{tag-input → grain}/files/src/lib/kit.tsx +8 -5
- package/registry/grain.json +17 -0
- package/registry/kit/files/src/lib/kit.tsx +8 -5
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +1 -1
- package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +1 -1
- package/registry/list-item/files/src/components/ui/list-item.tsx +20 -16
- package/registry/list-item/files/src/lib/kit.tsx +8 -5
- package/registry/morphing-button-group.json +18 -0
- package/registry/orb-reveal.json +20 -0
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +8 -6
- package/registry/otp-input/files/src/lib/kit.tsx +8 -5
- package/registry/password-input/files/src/components/ui/floating-input.tsx +16 -6
- package/registry/password-input/files/src/components/ui/password-input.tsx +2 -2
- package/registry/password-input/files/src/lib/kit.tsx +8 -5
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +1 -1
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +27 -14
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +2 -2
- package/registry/payment-card/files/src/lib/kit.tsx +8 -5
- package/registry/progress/files/src/components/ui/progress.tsx +8 -5
- package/registry/progress/files/src/lib/kit.tsx +8 -5
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +15 -11
- package/registry/progress-steps/files/src/lib/kit.tsx +8 -5
- package/registry/qr-code-button.json +18 -0
- package/registry/radio-group/files/src/components/ui/radio-group.tsx +10 -9
- package/registry/radio-group/files/src/lib/kit.tsx +8 -5
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +13 -11
- package/registry/search-bar/files/src/lib/kit.tsx +8 -5
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +4 -2
- package/registry/segmented-control/files/src/lib/kit.tsx +8 -5
- package/registry/select/files/src/components/ui/select.tsx +3 -3
- package/registry/select/files/src/lib/kit.tsx +8 -5
- package/registry/skeleton/files/src/components/ui/skeleton.tsx +4 -4
- package/registry/skeleton/files/src/lib/kit.tsx +8 -5
- package/registry/slider/files/src/components/ui/slider.tsx +5 -4
- package/registry/slider/files/src/lib/kit.tsx +8 -5
- package/registry/spinner/files/src/components/ui/spinner.tsx +452 -34
- package/registry/spinner/files/src/lib/kit.tsx +8 -5
- package/registry/spinner.json +1 -1
- package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +532 -0
- package/registry/{pagination-dots → stateful-button}/files/src/lib/kit.tsx +8 -5
- package/registry/stateful-button.json +18 -0
- package/registry/stepper/files/src/components/ui/stepper.tsx +8 -4
- package/registry/stepper/files/src/lib/kit.tsx +8 -5
- package/registry/switch/files/src/components/ui/switch.tsx +4 -3
- package/registry/switch/files/src/lib/kit.tsx +8 -5
- package/registry/tabs/files/src/components/ui/tabs.tsx +10 -8
- package/registry/tabs/files/src/lib/kit.tsx +8 -5
- package/registry/text-animation/files/src/components/ui/text-animation.tsx +650 -0
- package/registry/text-animation/files/src/lib/kit.tsx +222 -0
- package/registry/text-animation.json +15 -0
- package/registry/textarea/files/src/components/ui/textarea.tsx +11 -9
- package/registry/textarea/files/src/lib/kit.tsx +8 -5
- package/registry/ticket-shape/files/src/lib/kit.tsx +8 -5
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +2 -2
- package/registry/tilt-card/files/src/lib/kit.tsx +8 -5
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +2 -2
- package/registry/time-picker/files/src/lib/kit.tsx +8 -5
- package/registry/timeline/files/src/components/ui/avatar.tsx +7 -4
- package/registry/timeline/files/src/components/ui/timeline.tsx +12 -10
- package/registry/timeline/files/src/lib/kit.tsx +8 -5
- package/registry/toast/files/src/components/ui/button.tsx +4 -3
- package/registry/toast/files/src/components/ui/toast.tsx +13 -11
- package/registry/toast/files/src/lib/kit.tsx +8 -5
- package/schema/components.schema.json +1 -1
- package/registry/_docs/color-swatch.json +0 -66
- package/registry/_docs/kpi-card.json +0 -11
- package/registry/_docs/like-button.json +0 -72
- package/registry/_docs/morph-button.json +0 -108
- package/registry/_docs/pagination-dots.json +0 -88
- package/registry/_docs/shimmer-button.json +0 -78
- package/registry/_docs/tag-input.json +0 -128
- package/registry/color-swatch.json +0 -17
- package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +0 -45
- package/registry/kpi-card.json +0 -14
- package/registry/like-button.json +0 -18
- package/registry/morph-button.json +0 -18
- package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +0 -262
- package/registry/pagination-dots.json +0 -15
- package/registry/shimmer-button.json +0 -17
- package/registry/tag-input/files/src/components/ui/tag-input.tsx +0 -720
- package/registry/tag-input.json +0 -17
package/bin/lib.mjs
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Pure helpers for the expo-kit CLI (no I/O, no dependencies) — kept separate so they can be unit-tested.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
5
7
|
/** Thrown for user-facing errors; the CLI entry point prints `message` and exits 1. */
|
|
6
8
|
export class CliError extends Error {}
|
|
7
9
|
|
|
@@ -269,6 +271,27 @@ export function isValidItemName(name) {
|
|
|
269
271
|
return typeof name === 'string' && /^[a-z0-9][a-z0-9-]{0,99}$/.test(name);
|
|
270
272
|
}
|
|
271
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Tab names from the user's tabs.ts become route files in app/(tabs)/: letters, digits and route punctuation
|
|
276
|
+
* (`-_.()[]`) only, so a name can never be a path like `../../x`.
|
|
277
|
+
*/
|
|
278
|
+
export function isSafeTabName(name) {
|
|
279
|
+
return typeof name === 'string' && name.length <= 100 && /^[A-Za-z0-9_.()[\]-]+$/.test(name) && !/^\.+$/.test(name);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Project paths
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Whether `rel` (a components.json `paths` value) stays inside `root` once joined to it the way the CLI
|
|
288
|
+
* writes files: `../x` escapes, while `/src/lib` is re-rooted by path.join and stays inside.
|
|
289
|
+
*/
|
|
290
|
+
export function isInsideDir(root, rel) {
|
|
291
|
+
const r = path.relative(root, path.join(root, rel));
|
|
292
|
+
return r === '' || (r !== '..' && !r.startsWith(`..${path.sep}`) && !path.isAbsolute(r));
|
|
293
|
+
}
|
|
294
|
+
|
|
272
295
|
/**
|
|
273
296
|
* npm package name, optionally with a simple version (`@1.2.3`, `@~14.0.0`, `@^2`). It reaches
|
|
274
297
|
* `npx expo install` through a shell on Windows, so no `< > | =` or spaces — ranges like `>=1` are out.
|
|
@@ -288,6 +311,46 @@ export function kitVersion(text) {
|
|
|
288
311
|
return /export function KitProvider\b/.test(text) ? 1 : null;
|
|
289
312
|
}
|
|
290
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Packages a registry item may depend on that work in the Expo Go app (SDK 58): native modules Expo Go
|
|
316
|
+
* ships, plus pure-JS packages. Anything else needs a development build (`npx expo run:ios|android`).
|
|
317
|
+
* Keep in sync with the Expo Go module list when the SDK moves.
|
|
318
|
+
*/
|
|
319
|
+
export const EXPO_GO_PACKAGES = new Set([
|
|
320
|
+
'@expo/vector-icons',
|
|
321
|
+
'@shopify/react-native-skia',
|
|
322
|
+
'expo-blur',
|
|
323
|
+
'expo-charts',
|
|
324
|
+
'expo-glass-effect',
|
|
325
|
+
'expo-haptics',
|
|
326
|
+
'expo-router',
|
|
327
|
+
'expo-splash-screen',
|
|
328
|
+
'react-native-gesture-handler',
|
|
329
|
+
'react-native-reanimated',
|
|
330
|
+
'react-native-safe-area-context',
|
|
331
|
+
'react-native-screens',
|
|
332
|
+
'react-native-svg',
|
|
333
|
+
'react-native-worklets',
|
|
334
|
+
]);
|
|
335
|
+
|
|
336
|
+
/** Which of `deps` keep an item out of Expo Go (bare names; `pkg@1.2` is read as `pkg`). Empty: runs in Expo Go. */
|
|
337
|
+
export function needsDevBuild(deps) {
|
|
338
|
+
return (deps ?? [])
|
|
339
|
+
.map((spec) => spec.replace(/^(@?[^@]+)@.*$/, '$1'))
|
|
340
|
+
.filter((name) => !EXPO_GO_PACKAGES.has(name));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The component a file is named for: the PascalCase export matching one of `names` ("Grain Gradient" or
|
|
345
|
+
* "grain-gradient" → GrainGradient), else the first PascalCase export (never a CONSTANT).
|
|
346
|
+
*/
|
|
347
|
+
export function pickMainExport(source, names = []) {
|
|
348
|
+
const exports = [...source.matchAll(/export (?:function|const) ([A-Z][A-Za-z0-9]*[a-z][A-Za-z0-9]*)\b/g)].map((m) => m[1]);
|
|
349
|
+
const pascal = (s) => String(s ?? '').split(/[^A-Za-z0-9]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join('');
|
|
350
|
+
const wanted = names.map(pascal).map((n) => n.toLowerCase());
|
|
351
|
+
return exports.find((e) => wanted.includes(e.toLowerCase())) ?? exports[0] ?? null;
|
|
352
|
+
}
|
|
353
|
+
|
|
291
354
|
/** Union of dependency lists, order preserved. */
|
|
292
355
|
export function mergeDependencies(...lists) {
|
|
293
356
|
const out = [];
|
|
@@ -301,7 +364,7 @@ export function mergeDependencies(...lists) {
|
|
|
301
364
|
// Site URL + token guard
|
|
302
365
|
// ---------------------------------------------------------------------------
|
|
303
366
|
|
|
304
|
-
export const DEFAULT_SITE_URL = 'https://expokit.
|
|
367
|
+
export const DEFAULT_SITE_URL = 'https://expokit.in';
|
|
305
368
|
|
|
306
369
|
export function isLoopbackHost(hostname) {
|
|
307
370
|
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname === '::1';
|
|
@@ -341,3 +404,49 @@ export function canSendToken({ siteUrl, credsUrl, fromEnv = false, defaultUrl =
|
|
|
341
404
|
if (fromEnv) return isLoopbackHost(new URL(siteUrl).hostname);
|
|
342
405
|
return Boolean(credsUrl) && target === originOf(credsUrl);
|
|
343
406
|
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* `env` without EXPO_KIT_TOKEN, for child processes: `npx expo install` runs third-party install scripts,
|
|
410
|
+
* and none of them needs the Pro token. Case-insensitive, as Windows env names are.
|
|
411
|
+
*/
|
|
412
|
+
export function envWithoutToken(env) {
|
|
413
|
+
return Object.fromEntries(Object.entries(env).filter(([key]) => key.toUpperCase() !== 'EXPO_KIT_TOKEN'));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
// 429 / 503: wait once, then give up
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
/** The longest the CLI waits before its one retry of a 429 / 503. */
|
|
421
|
+
export const RETRY_MAX_WAIT_MS = 10_000;
|
|
422
|
+
|
|
423
|
+
/** A Retry-After header (delta-seconds or an HTTP date) in whole seconds from `now`; null when missing or unreadable. */
|
|
424
|
+
export function parseRetryAfter(value, now = Date.now()) {
|
|
425
|
+
if (value === null || value === undefined) return null;
|
|
426
|
+
const text = String(value).trim();
|
|
427
|
+
if (/^\d+$/.test(text)) return Number(text);
|
|
428
|
+
if (!/[A-Za-z]/.test(text)) return null; // an HTTP date always names its day / month
|
|
429
|
+
const at = Date.parse(text);
|
|
430
|
+
return Number.isNaN(at) ? null : Math.max(0, Math.ceil((at - now) / 1000));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Milliseconds to wait before retrying a 429 / 503 once: what the server asked (3 s when it didn't say).
|
|
435
|
+
* null when it asked for longer than RETRY_MAX_WAIT_MS: retrying sooner than that just earns another 429 / 503.
|
|
436
|
+
*/
|
|
437
|
+
export function retryDelayMs(retryAfter, now = Date.now()) {
|
|
438
|
+
const secs = parseRetryAfter(retryAfter, now);
|
|
439
|
+
if (secs === null) return 3000;
|
|
440
|
+
if (secs * 1000 > RETRY_MAX_WAIT_MS) return null;
|
|
441
|
+
return secs * 1000;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** "in 30 s" / "in 5 min" / "in about 2 h" for a "try again …" message; "in a minute" when the server didn't say. */
|
|
445
|
+
export function retryHint(retryAfter, now = Date.now()) {
|
|
446
|
+
const secs = parseRetryAfter(retryAfter, now);
|
|
447
|
+
if (secs === null) return 'in a minute';
|
|
448
|
+
if (secs < 90) return `in ${Math.max(1, secs)} s`;
|
|
449
|
+
if (secs < 90 * 60) return `in ${Math.round(secs / 60)} min`;
|
|
450
|
+
if (secs < 48 * 3600) return `in about ${Math.round(secs / 3600)} h`;
|
|
451
|
+
return 'later';
|
|
452
|
+
}
|
package/bin/mcp.mjs
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import fs from 'node:fs';
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
|
|
13
|
+
import { needsDevBuild } from './lib.mjs';
|
|
14
|
+
|
|
13
15
|
/** Newest first; we answer with the client's version when we know it, else our newest. */
|
|
14
16
|
export const PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
|
|
15
17
|
|
|
@@ -21,7 +23,7 @@ Items are copied into the user's app as source files by the expo-kit CLI — the
|
|
|
21
23
|
Workflow: search_items or list_items → view_item (props, import line, examples) → get_add_command, then run that command in the app's root folder.
|
|
22
24
|
The CLI creates components.json, adds the @/* tsconfig alias, copies files under src/ (or the root app layout) and installs npm deps with \`npx expo install\`.
|
|
23
25
|
Import components from '@/components/...' and wrap the app in <KitProvider> from '@/lib/kit' (added with the first item).
|
|
24
|
-
Pro items need an ExpoKit Pro plan: the user runs \`npx ${PACKAGE}@latest login\` once (token from https://expokit.
|
|
26
|
+
Pro items need an ExpoKit Pro plan: the user runs \`npx ${PACKAGE}@latest login\` once (token from https://expokit.in/account). Never ask the user to paste a token into chat.`;
|
|
25
27
|
|
|
26
28
|
// ---------------------------------------------------------------------------
|
|
27
29
|
// Registry access (bundled JSON)
|
|
@@ -179,7 +181,7 @@ function searchItems(catalog, { query }) {
|
|
|
179
181
|
function viewItem(catalog, registryDir, { name, include_source: includeSource = false }) {
|
|
180
182
|
const it = requireItem(catalog, name);
|
|
181
183
|
const doc = catalog.docs.get(it.name);
|
|
182
|
-
const out = [`# ${it.title} (${it.name})`, '', it.description, ''];
|
|
184
|
+
const out = [`# ${it.title} (${it.name})`, '', ...(doc?.summary ? [doc.summary, ''] : []), it.description, ''];
|
|
183
185
|
out.push(`Type: ${it.type} · Tier: ${it.tier === 'pro' ? 'PRO (needs an ExpoKit Pro plan)' : 'free'}${doc?.category ? ` · Category: ${doc.category}` : ''}`);
|
|
184
186
|
if (doc?.aliases?.length) out.push(`Aliases: ${doc.aliases.join(', ')}`);
|
|
185
187
|
out.push('', `Add: \`${addCommand([it.name])}\``);
|
|
@@ -187,6 +189,8 @@ function viewItem(catalog, registryDir, { name, include_source: includeSource =
|
|
|
187
189
|
if (imp) out.push(`Import: \`${imp}\``);
|
|
188
190
|
if (doc?.files?.length) out.push(`Files it adds: ${doc.files.join(', ')}`);
|
|
189
191
|
if (it.dependencies.length) out.push(`npm deps (installed by the CLI): ${it.dependencies.join(', ')}`);
|
|
192
|
+
const native = needsDevBuild(it.dependencies);
|
|
193
|
+
out.push(native.length ? `Runs in Expo Go: no, needs a development build (${native.join(', ')})` : 'Runs in Expo Go: yes');
|
|
190
194
|
if (it.registryDependencies.length) out.push(`Also adds: ${it.registryDependencies.join(', ')}`);
|
|
191
195
|
|
|
192
196
|
if (doc?.props?.length) {
|
|
@@ -195,11 +199,18 @@ function viewItem(catalog, registryDir, { name, include_source: includeSource =
|
|
|
195
199
|
for (const [prop, type, def, desc] of doc.props) out.push(`| ${cell(prop)} | ${cell(type)} | ${cell(def)} | ${cell(desc)} |`);
|
|
196
200
|
}
|
|
197
201
|
|
|
202
|
+
if (doc?.behavior?.length) {
|
|
203
|
+
out.push('', '## Behaviour & accessibility', '');
|
|
204
|
+
for (const [label, text] of doc.behavior) out.push(`- **${label}:** ${text}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (doc?.usage) out.push('', '## Usage', '', '```tsx', doc.usage.trimEnd(), '```');
|
|
208
|
+
|
|
198
209
|
if (doc?.examples?.length) {
|
|
199
210
|
out.push('', '## Examples');
|
|
200
211
|
for (const ex of doc.examples) out.push('', `### ${ex.title}`, '', '```tsx', ex.code.trimEnd(), '```');
|
|
201
212
|
} else if (it.tier === 'pro') {
|
|
202
|
-
out.push('', 'Usage examples and source for Pro items are on https://expokit.
|
|
213
|
+
out.push('', 'Usage examples and source for Pro items are on https://expokit.in after signing in, and in the files the add command copies.');
|
|
203
214
|
}
|
|
204
215
|
|
|
205
216
|
if (includeSource) {
|
|
@@ -233,7 +244,7 @@ function getAddCommand(catalog, { names }) {
|
|
|
233
244
|
];
|
|
234
245
|
if (pro.length)
|
|
235
246
|
lines.push(
|
|
236
|
-
`- PRO: ${pro.join(', ')} ${pro.length === 1 ? 'needs' : 'need'} an ExpoKit Pro plan. If the command reports "not logged in", ask the user to run \`npx ${PACKAGE}@latest login\` themselves (token from https://expokit.
|
|
247
|
+
`- PRO: ${pro.join(', ')} ${pro.length === 1 ? 'needs' : 'need'} an ExpoKit Pro plan. If the command reports "not logged in", ask the user to run \`npx ${PACKAGE}@latest login\` themselves (token from https://expokit.in/account). Never ask for the token in chat.`
|
|
237
248
|
);
|
|
238
249
|
return lines.join('\n');
|
|
239
250
|
}
|
|
@@ -258,15 +269,19 @@ export function callTool(catalog, registryDir, name, args = {}) {
|
|
|
258
269
|
// ---------------------------------------------------------------------------
|
|
259
270
|
|
|
260
271
|
const rpcError = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
|
|
272
|
+
const isObject = (v) => Boolean(v) && typeof v === 'object' && !Array.isArray(v);
|
|
261
273
|
|
|
262
274
|
/** One incoming JSON-RPC message → the response object, or null for notifications. */
|
|
263
275
|
export function handleMessage(msg, ctx) {
|
|
264
|
-
if (!msg ||
|
|
265
|
-
|
|
276
|
+
if (!isObject(msg) || msg.jsonrpc !== '2.0' || typeof msg.method !== 'string') {
|
|
277
|
+
// A stray `5`, "abc" or nested array has no id to answer; an object with one gets an error back.
|
|
278
|
+
return isObject(msg) && 'id' in msg ? rpcError(msg.id ?? null, -32600, 'Invalid request') : null;
|
|
266
279
|
}
|
|
267
280
|
const isRequest = 'id' in msg && msg.id !== null;
|
|
268
281
|
if (!isRequest) return null; // notifications/initialized, notifications/cancelled, …
|
|
269
|
-
const { id, method
|
|
282
|
+
const { id, method } = msg;
|
|
283
|
+
// `params: null` skips a destructuring default, and params.name would then throw.
|
|
284
|
+
const params = isObject(msg.params) ? msg.params : {};
|
|
270
285
|
const ok = (result) => ({ jsonrpc: '2.0', id, result });
|
|
271
286
|
|
|
272
287
|
switch (method) {
|
|
@@ -306,6 +321,30 @@ export function handleMessage(msg, ctx) {
|
|
|
306
321
|
}
|
|
307
322
|
}
|
|
308
323
|
|
|
324
|
+
/**
|
|
325
|
+
* One stdin line → what to write back: a reply, an array of replies for a batch, or null. Never throws —
|
|
326
|
+
* it runs inside the stdin listener, where one bad line would otherwise kill the server for the session.
|
|
327
|
+
*/
|
|
328
|
+
export function handleLine(line, ctx) {
|
|
329
|
+
let msg;
|
|
330
|
+
try {
|
|
331
|
+
msg = JSON.parse(line);
|
|
332
|
+
} catch {
|
|
333
|
+
return rpcError(null, -32700, 'Parse error');
|
|
334
|
+
}
|
|
335
|
+
const reply = (m) => {
|
|
336
|
+
try {
|
|
337
|
+
return handleMessage(m, ctx);
|
|
338
|
+
} catch {
|
|
339
|
+
return isObject(m) && 'id' in m && m.id !== null ? rpcError(m.id, -32603, 'Internal error') : null;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
if (!Array.isArray(msg)) return reply(msg);
|
|
343
|
+
// Batches came and went in 2025-03-26; answer them anyway.
|
|
344
|
+
const replies = msg.map(reply).filter(Boolean);
|
|
345
|
+
return replies.length ? replies : null;
|
|
346
|
+
}
|
|
347
|
+
|
|
309
348
|
/** Serves MCP on stdin/stdout until stdin closes. */
|
|
310
349
|
export function startMcpServer({ registryDir, version, aliases }) {
|
|
311
350
|
const ctx = { registryDir, version, catalog: loadCatalog(registryDir, aliases) };
|
|
@@ -319,18 +358,8 @@ export function startMcpServer({ registryDir, version, aliases }) {
|
|
|
319
358
|
const line = buffer.slice(0, nl).trim();
|
|
320
359
|
buffer = buffer.slice(nl + 1);
|
|
321
360
|
if (!line) continue;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
msg = JSON.parse(line);
|
|
325
|
-
} catch {
|
|
326
|
-
send(rpcError(null, -32700, 'Parse error'));
|
|
327
|
-
continue;
|
|
328
|
-
}
|
|
329
|
-
// Batches came and went in 2025-03-26; answer them anyway.
|
|
330
|
-
const replies = (Array.isArray(msg) ? msg : [msg]).map((m) => handleMessage(m, ctx)).filter(Boolean);
|
|
331
|
-
if (Array.isArray(msg)) {
|
|
332
|
-
if (replies.length) send(replies);
|
|
333
|
-
} else if (replies[0]) send(replies[0]);
|
|
361
|
+
const reply = handleLine(line, ctx);
|
|
362
|
+
if (reply) send(reply);
|
|
334
363
|
}
|
|
335
364
|
});
|
|
336
365
|
process.stdin.on('end', () => process.exit(0));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expokit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "shadcn-style CLI for Expo — add native UI blocks and components to your app",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"bin",
|
|
12
12
|
"registry",
|
|
13
13
|
"schema",
|
|
14
|
-
"README.md"
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
15
16
|
],
|
|
16
17
|
"keywords": [
|
|
17
18
|
"expo",
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"engines": {
|
|
27
28
|
"node": ">=18"
|
|
28
29
|
},
|
|
29
|
-
"homepage": "https://expokit.
|
|
30
|
+
"homepage": "https://expokit.in",
|
|
30
31
|
"repository": {
|
|
31
32
|
"type": "git",
|
|
32
33
|
"url": "git+https://github.com/Anshkaran7/expokit.git",
|
|
@@ -35,7 +36,11 @@
|
|
|
35
36
|
"bugs": {
|
|
36
37
|
"url": "https://github.com/Anshkaran7/expokit/issues"
|
|
37
38
|
},
|
|
38
|
-
"
|
|
39
|
+
"funding": {
|
|
40
|
+
"type": "github",
|
|
41
|
+
"url": "https://github.com/sponsors/Anshkaran7"
|
|
42
|
+
},
|
|
43
|
+
"author": "Karan (anshkaran7) <work@karandev.in>",
|
|
39
44
|
"publishConfig": {
|
|
40
45
|
"access": "public"
|
|
41
46
|
},
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"files": [
|
|
7
7
|
"src/components/ui/accordion.tsx"
|
|
8
8
|
],
|
|
9
|
+
"summary": "Sections spring open to their measured height while the chevron turns.",
|
|
9
10
|
"props": [
|
|
10
11
|
[
|
|
11
12
|
"type",
|
|
@@ -74,6 +75,21 @@
|
|
|
74
75
|
"Style for the Accordion's root view (the card in the bordered variant); AccordionItem takes its own."
|
|
75
76
|
]
|
|
76
77
|
],
|
|
78
|
+
"behavior": [
|
|
79
|
+
[
|
|
80
|
+
"Touch",
|
|
81
|
+
"Tap a header to open or close its section. In single mode, opening one closes the other; disabled items don't respond."
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
"Motion",
|
|
85
|
+
"Content height springs to its measured size, so any content animates smoothly. The chevron turns 180° on the native driver."
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
"Screen readers",
|
|
89
|
+
"Each header is a button with expanded and disabled state. Content of closed sections is hidden from assistive tech."
|
|
90
|
+
]
|
|
91
|
+
],
|
|
92
|
+
"usage": "import { Accordion, AccordionItem } from '@/components/ui/accordion';\n\n<Accordion defaultValue={['shipping']}>\n <AccordionItem value=\"shipping\" title=\"How long does shipping take?\">\n Orders leave our warehouse within 24 hours.\n </AccordionItem>\n <AccordionItem value=\"returns\" title=\"Can I return an item?\">\n You have 30 days to send anything back for a full refund.\n </AccordionItem>\n</Accordion>",
|
|
77
93
|
"examples": [
|
|
78
94
|
{
|
|
79
95
|
"name": "Default",
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"files": [
|
|
7
7
|
"src/components/ui/action-sheet.tsx"
|
|
8
8
|
],
|
|
9
|
+
"summary": "An iOS-style action sheet that follows your finger and flings away to dismiss.",
|
|
9
10
|
"props": [
|
|
10
11
|
[
|
|
11
12
|
"open / defaultOpen",
|
|
@@ -80,6 +81,33 @@
|
|
|
80
81
|
"Sheet container style."
|
|
81
82
|
]
|
|
82
83
|
],
|
|
84
|
+
"behavior": [
|
|
85
|
+
[
|
|
86
|
+
"Touch",
|
|
87
|
+
"Drag anywhere on the sheet. Pull past a third of its height or flick down to dismiss; pulling up rubber-bands."
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
"Motion",
|
|
91
|
+
"Springs up over a fading scrim. The exit is quicker than the entry and keeps a fling's momentum."
|
|
92
|
+
],
|
|
93
|
+
[
|
|
94
|
+
"Reduced motion",
|
|
95
|
+
"The sheet fades in and out in place instead of sliding up from the bottom."
|
|
96
|
+
],
|
|
97
|
+
[
|
|
98
|
+
"Haptics",
|
|
99
|
+
"A light impact when it opens and a selection tick when you pick an action. Turn off with haptics={false}."
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
"Screen readers",
|
|
103
|
+
"The sheet is a modal view, the title is a header and each row is a button with its label and disabled state."
|
|
104
|
+
],
|
|
105
|
+
[
|
|
106
|
+
"Keyboard",
|
|
107
|
+
"Escape closes it on web; the Android back button closes it too."
|
|
108
|
+
]
|
|
109
|
+
],
|
|
110
|
+
"usage": "import { useState } from 'react';\nimport { ActionSheet } from '@/components/ui/action-sheet';\n\nconst [open, setOpen] = useState(false);\n<ActionSheet\n open={open}\n onOpenChange={setOpen}\n title=\"Profile photo\"\n actions={[{ label: 'Take photo' }, { label: 'Remove photo', destructive: true }]}\n/>",
|
|
83
111
|
"examples": [
|
|
84
112
|
{
|
|
85
113
|
"name": "Photo",
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"files": [
|
|
7
7
|
"src/components/ui/alert.tsx"
|
|
8
8
|
],
|
|
9
|
+
"summary": "An inline banner whose icon draws itself inside a halo, with an optional draining timer.",
|
|
9
10
|
"props": [
|
|
10
11
|
[
|
|
11
12
|
"variant",
|
|
@@ -92,6 +93,33 @@
|
|
|
92
93
|
"Style for the outer animated wrapper (its height collapses on dismiss)."
|
|
93
94
|
]
|
|
94
95
|
],
|
|
96
|
+
"behavior": [
|
|
97
|
+
[
|
|
98
|
+
"Motion",
|
|
99
|
+
"On appear the icon outline draws, the glyph springs in and the halo ripples once. Dismissing collapses the height so content below glides up."
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
"Touch",
|
|
103
|
+
"With duration set, a hairline drains along the bottom to show the time left. Pressing the alert pauses it."
|
|
104
|
+
],
|
|
105
|
+
[
|
|
106
|
+
"Reduced motion",
|
|
107
|
+
"The icon draw, glyph pop and halo ripple jump straight to their finished state."
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
"Haptics",
|
|
111
|
+
"Success, warning and danger alerts play the matching notification haptic when they appear; info stays silent."
|
|
112
|
+
],
|
|
113
|
+
[
|
|
114
|
+
"Screen readers",
|
|
115
|
+
"The card has the alert role in a polite live region. The close button is labelled Dismiss and actions are buttons named by their label."
|
|
116
|
+
],
|
|
117
|
+
[
|
|
118
|
+
"Web",
|
|
119
|
+
"Hovering also pauses the auto-dismiss timer."
|
|
120
|
+
]
|
|
121
|
+
],
|
|
122
|
+
"usage": "import { Alert } from '@/components/ui/alert';\n\n<Alert\n variant=\"warning\"\n title=\"Storage almost full\"\n description=\"You've used 9.4 GB of your 10 GB plan.\"\n dismissible\n/>",
|
|
95
123
|
"examples": [
|
|
96
124
|
{
|
|
97
125
|
"name": "Soft",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "assistant-app",
|
|
3
|
+
"category": "Templates",
|
|
4
|
+
"aliases": [],
|
|
5
|
+
"exportName": null,
|
|
6
|
+
"files": [
|
|
7
|
+
"src/app/(tabs)/_layout.tsx",
|
|
8
|
+
"src/app/(tabs)/history.tsx",
|
|
9
|
+
"src/app/(tabs)/index.tsx",
|
|
10
|
+
"src/app/(tabs)/profile.tsx",
|
|
11
|
+
"src/app/_layout.tsx",
|
|
12
|
+
"src/app/onboarding.tsx",
|
|
13
|
+
"src/app/settings.tsx",
|
|
14
|
+
"src/app/sign-in.tsx",
|
|
15
|
+
"src/components/assistant-app/tabs.tsx",
|
|
16
|
+
"src/lib/assistant-app/assistant.ts",
|
|
17
|
+
"src/lib/assistant-app/conversations.tsx",
|
|
18
|
+
"src/lib/assistant-app/data.ts",
|
|
19
|
+
"src/lib/assistant-app/session.tsx"
|
|
20
|
+
],
|
|
21
|
+
"summary": null,
|
|
22
|
+
"props": [],
|
|
23
|
+
"behavior": [
|
|
24
|
+
[
|
|
25
|
+
"Flow",
|
|
26
|
+
"Onboarding shows once, then sign in (Apple, Google or a 6-digit email code), then the tabs. Signing out returns to sign in. The session is saved on the device (AsyncStorage) and guarded with Expo Router’s Stack.Protected."
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
"Chat",
|
|
30
|
+
"Fully controlled: your message goes in, the reply streams in word by word with a Stop button, and suggestion chips fill the empty state. The tab bar steps aside while the keyboard is up."
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
"History",
|
|
34
|
+
"Every conversation, newest first. Tap to reopen it in Chat, swipe to delete, or compose a new one."
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
"Settings",
|
|
38
|
+
"Pushed from Profile. Appearance (System / Light / Dark) re-themes every kit component instantly."
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
"Your model",
|
|
42
|
+
"lib/assistant-app/assistant.ts streams a canned answer so the app runs with no backend. Replace `reply` with a call to your server, which calls the model API — never ship an API key in the app."
|
|
43
|
+
]
|
|
44
|
+
],
|
|
45
|
+
"usage": null,
|
|
46
|
+
"examples": []
|
|
47
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"files": [
|
|
7
7
|
"src/components/ui/avatar.tsx"
|
|
8
8
|
],
|
|
9
|
+
"summary": "A photo over hashed-colour initials, with presence dots and badges cut into the edge.",
|
|
9
10
|
"props": [
|
|
10
11
|
[
|
|
11
12
|
"src",
|
|
@@ -158,11 +159,34 @@
|
|
|
158
159
|
"Style for the avatar root; AvatarGroup applies it to its row."
|
|
159
160
|
]
|
|
160
161
|
],
|
|
162
|
+
"behavior": [
|
|
163
|
+
[
|
|
164
|
+
"Motion",
|
|
165
|
+
"The photo fades in over the initials, a story ring cross-fades to a hairline once seen, and AvatarGroup springs its faces in with a stagger."
|
|
166
|
+
],
|
|
167
|
+
[
|
|
168
|
+
"Touch",
|
|
169
|
+
"With onPress it becomes a button that springs down while pressed."
|
|
170
|
+
],
|
|
171
|
+
[
|
|
172
|
+
"Reduced motion",
|
|
173
|
+
"The status pulse, ring spin, press squish and group stagger are skipped, and the seen cross-fade is instant."
|
|
174
|
+
],
|
|
175
|
+
[
|
|
176
|
+
"Haptics",
|
|
177
|
+
"A light impact on press when onPress is set. Turn off with haptics={false}."
|
|
178
|
+
],
|
|
179
|
+
[
|
|
180
|
+
"Screen readers",
|
|
181
|
+
"Read as an image (or a button when pressable) labelled with the name plus verified, status, notification and story details. A group reads its first names and \"and N more\"."
|
|
182
|
+
]
|
|
183
|
+
],
|
|
184
|
+
"usage": "import { Avatar, AvatarGroup } from '@/components/ui/avatar';\n\n<Avatar name=\"Ava Rossi\" src=\"https://i.pravatar.cc/240?img=47\" size=\"lg\" status=\"online\" />\n<AvatarGroup max={3} total={12}>\n <Avatar name=\"Noah Kim\" />\n <Avatar name=\"Priya Patel\" />\n <Avatar name=\"Diego Alvarez\" />\n</AvatarGroup>",
|
|
161
185
|
"examples": [
|
|
162
186
|
{
|
|
163
187
|
"name": "ProfileHeader",
|
|
164
188
|
"title": "Profile Header",
|
|
165
|
-
"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 ProfileHeader() {\n const { colors, radius, dark } = useKit();\n return (\n <View\n style={
|
|
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"
|
|
166
190
|
},
|
|
167
191
|
{
|
|
168
192
|
"name": "ChatList",
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
"name": "badge",
|
|
3
3
|
"category": "Display",
|
|
4
4
|
"aliases": [],
|
|
5
|
-
"exportName": "
|
|
5
|
+
"exportName": "Badge",
|
|
6
6
|
"files": [
|
|
7
7
|
"src/components/ui/badge.tsx"
|
|
8
8
|
],
|
|
9
|
+
"summary": "Status pills in four styles that roll, pop and re-tint when their status changes.",
|
|
9
10
|
"props": [
|
|
10
11
|
[
|
|
11
12
|
"variant",
|
|
@@ -49,6 +50,12 @@
|
|
|
49
50
|
"false",
|
|
50
51
|
"Pulsing dot for live states (dot variant)."
|
|
51
52
|
],
|
|
53
|
+
[
|
|
54
|
+
"animateChanges",
|
|
55
|
+
"boolean",
|
|
56
|
+
"true",
|
|
57
|
+
"Roll the label, pop the icon, cross-fade colours and spring the width when they change. Never on mount."
|
|
58
|
+
],
|
|
52
59
|
[
|
|
53
60
|
"NotificationBadge.count",
|
|
54
61
|
"number",
|
|
@@ -92,26 +99,50 @@
|
|
|
92
99
|
"Style for the badge pill; NotificationBadge applies it to the anchor wrapper or bare bubble wrapper."
|
|
93
100
|
]
|
|
94
101
|
],
|
|
102
|
+
"behavior": [
|
|
103
|
+
[
|
|
104
|
+
"Motion",
|
|
105
|
+
"On a change the old label rolls up and out as the new one rolls in (per letter for short Latin text), the icon swaps with a scale-and-rotate pop and the pill width springs to fit. Rolls and pops use the native driver; colour and width run on the JS driver on separate views. Nothing animates on mount; animateChanges={false} swaps instantly."
|
|
106
|
+
],
|
|
107
|
+
[
|
|
108
|
+
"Reduced motion",
|
|
109
|
+
"Changes swap instantly: no roll, pop, colour fade or width spring. The dot variant's pulse still loops."
|
|
110
|
+
],
|
|
111
|
+
[
|
|
112
|
+
"Screen readers",
|
|
113
|
+
"Text labels are one accessible element whose label is always the current text; on Android it is a polite live region so status changes are announced."
|
|
114
|
+
],
|
|
115
|
+
[
|
|
116
|
+
"Platforms",
|
|
117
|
+
"React Native Animated and react-native-svg only — iOS, Android, Expo Go and web."
|
|
118
|
+
]
|
|
119
|
+
],
|
|
120
|
+
"usage": "import { Badge } from '@/components/ui/badge';\n\nexport function BuildStatus({ done }: { done: boolean }) {\n return (\n <Badge tone={done ? 'success' : 'warning'}>{done ? 'Deployed' : 'Building'}</Badge>\n );\n}",
|
|
95
121
|
"examples": [
|
|
96
122
|
{
|
|
97
123
|
"name": "Variants",
|
|
98
124
|
"title": "Variants",
|
|
99
|
-
"code": "import { Pressable, Text, View } from 'react-native';\nimport { Badge, BadgeIcon, BadgeIconTile, NotificationBadge } from '@/components/ui/badge';\n\nconst TONES = ['neutral', 'accent', 'success', 'warning', 'danger'] as const;\n\nexport default function Variants() {\n return (\n <View style={{ gap: 10 }}>\n {(['solid', 'soft', 'outline', 'dot'] as const).map((variant) => (\n <View key={variant} style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6 }}>\n {TONES.map((tone) => (\n <Badge key={tone} variant={variant} tone={tone}>\n {tone[0].toUpperCase() + tone.slice(1)}\n </Badge>\n ))}\n </View>\n ))}\n </View>\n );\n}\n"
|
|
125
|
+
"code": "import { Pressable, Text, View } from 'react-native';\nimport { Badge, BadgeIcon, BadgeIconTile, NotificationBadge, type BadgeIconName, type BadgeTone } from '@/components/ui/badge';\n\nconst TONES = ['neutral', 'accent', 'success', 'warning', 'danger'] as const;\n\nexport default function Variants() {\n return (\n <View style={{ gap: 10 }}>\n {(['solid', 'soft', 'outline', 'dot'] as const).map((variant) => (\n <View key={variant} style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6 }}>\n {TONES.map((tone) => (\n <Badge key={tone} variant={variant} tone={tone}>\n {tone[0].toUpperCase() + tone.slice(1)}\n </Badge>\n ))}\n </View>\n ))}\n </View>\n );\n}\n"
|
|
100
126
|
},
|
|
101
127
|
{
|
|
102
128
|
"name": "Statuses",
|
|
103
129
|
"title": "Statuses",
|
|
104
|
-
"code": "import { Pressable, Text, View } from 'react-native';\nimport { Badge, BadgeIcon, BadgeIconTile, NotificationBadge } from '@/components/ui/badge';\nimport { alpha, useKit } from '@/lib/kit';\n\nexport default function Statuses() {\n const { colors } = useKit();\n return (\n <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>\n <Badge variant=\"dot\" tone=\"success\" pulse>Live</Badge>\n <Badge variant=\"soft\" tone=\"success\" icon={<BadgeIcon name=\"package\" color={colors.success} size={14} />}>Paid</Badge>\n <Badge variant=\"soft\" tone=\"warning\">Pending review</Badge>\n <Badge variant=\"outline\" tone=\"danger\">Overdue</Badge>\n <Badge variant=\"solid\" tone=\"accent\" shape=\"rounded\">PRO</Badge>\n <Badge variant=\"soft\" tone=\"accent\" icon={<BadgeIcon name=\"heart\" color={colors.accent} size={14} />}>Saved</Badge>\n </View>\n );\n}\n"
|
|
130
|
+
"code": "import { 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 Statuses() {\n const { colors } = useKit();\n return (\n <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>\n <Badge variant=\"dot\" tone=\"success\" pulse>Live</Badge>\n <Badge variant=\"soft\" tone=\"success\" icon={<BadgeIcon name=\"package\" color={colors.success} size={14} />}>Paid</Badge>\n <Badge variant=\"soft\" tone=\"warning\">Pending review</Badge>\n <Badge variant=\"outline\" tone=\"danger\">Overdue</Badge>\n <Badge variant=\"solid\" tone=\"accent\" shape=\"rounded\">PRO</Badge>\n <Badge variant=\"soft\" tone=\"accent\" icon={<BadgeIcon name=\"heart\" color={colors.accent} size={14} />}>Saved</Badge>\n </View>\n );\n}\n"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"name": "LiveStatus",
|
|
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"
|
|
105
136
|
},
|
|
106
137
|
{
|
|
107
138
|
"name": "Sizes",
|
|
108
139
|
"title": "Sizes",
|
|
109
|
-
"code": "import { Pressable, Text, View } from 'react-native';\nimport { Badge, BadgeIcon, BadgeIconTile, NotificationBadge } 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', 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"
|
|
110
141
|
},
|
|
111
142
|
{
|
|
112
143
|
"name": "Notifications",
|
|
113
144
|
"title": "Notifications",
|
|
114
|
-
"code": "import React, { useState } from 'react';\nimport { Pressable, Text, View } from 'react-native';\nimport { Badge, BadgeIcon, BadgeIconTile, NotificationBadge } 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, height: 36, borderRadius: radius.capsule(36), justifyContent: 'center', backgroundColor: colors.muted }}\n >\n <Text style={{ fontWeight: '400', color: colors.foreground }}>{label as string}</Text>\n </Pressable>\n ))}\n </View>\n </View>\n );\n}\n"
|
|
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, height: 36, borderRadius: radius.capsule(36), justifyContent: 'center', backgroundColor: colors.muted }}\n >\n <Text style={{ fontWeight: '400', color: colors.foreground }}>{label as string}</Text>\n </Pressable>\n ))}\n </View>\n </View>\n );\n}\n"
|
|
115
146
|
}
|
|
116
147
|
]
|
|
117
148
|
}
|