@expokit/cli 0.2.0 → 0.3.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 +14 -0
- package/bin/expo-kit.js +86 -24
- package/bin/mcp.mjs +8 -1
- package/package.json +8 -3
- 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/avatar.json +24 -0
- package/registry/_docs/badge.json +35 -4
- package/registry/_docs/bottom-nav.json +24 -0
- package/registry/_docs/bottom-sheet.json +28 -0
- package/registry/_docs/bullet.json +3 -0
- package/registry/_docs/button-3d.json +16 -0
- package/registry/_docs/button.json +16 -0
- package/registry/_docs/calendar.json +16 -0
- 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 +3 -0
- package/registry/_docs/chip-group.json +16 -0
- package/registry/_docs/color-swatch.json +16 -0
- package/registry/_docs/confetti.json +20 -0
- 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 +20 -0
- package/registry/_docs/dialog.json +24 -0
- package/registry/_docs/drawer.json +28 -0
- package/registry/_docs/fab.json +24 -0
- package/registry/_docs/flip-card.json +16 -0
- package/registry/_docs/floating-input.json +20 -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 +16 -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/kpi-card.json +3 -0
- package/registry/_docs/like-button.json +20 -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/morph-button.json +20 -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/otp-input.json +24 -0
- package/registry/_docs/pagination-dots.json +20 -0
- package/registry/_docs/parallax-layers.json +20 -0
- package/registry/_docs/parallax-scroll.json +24 -0
- package/registry/_docs/password-input.json +20 -0
- 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/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 +20 -0
- package/registry/_docs/search-results.json +3 -0
- package/registry/_docs/segmented-control.json +20 -0
- package/registry/_docs/select.json +26 -1
- package/registry/_docs/settings.json +3 -0
- package/registry/_docs/shimmer-button.json +20 -0
- package/registry/_docs/sign-in.json +3 -0
- package/registry/_docs/skeleton.json +20 -0
- package/registry/_docs/slide-to-confirm.json +24 -0
- package/registry/_docs/slider.json +21 -1
- 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 +16 -0
- 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/tag-input.json +20 -0
- package/registry/_docs/text-animation.json +141 -0
- package/registry/_docs/textarea.json +16 -0
- package/registry/_docs/ticket-card.json +20 -0
- package/registry/_docs/ticket-shape.json +3 -0
- package/registry/_docs/tilt-card.json +16 -0
- package/registry/_docs/time-picker.json +16 -0
- package/registry/_docs/timeline-chart.json +3 -0
- package/registry/_docs/timeline.json +16 -0
- package/registry/_docs/toast.json +24 -0
- package/registry/_docs/treemap.json +3 -0
- package/registry/_docs/waterfall.json +3 -0
- package/registry/_docs/wheel-list.json +20 -0
- package/registry/badge/files/src/components/ui/badge.tsx +327 -17
- package/registry/badge.json +1 -1
- package/registry/chat.json +18 -0
- package/registry/spinner/files/src/components/ui/spinner.tsx +435 -22
- package/registry/spinner.json +1 -1
- package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +532 -0
- package/registry/stateful-button/files/src/lib/kit.tsx +219 -0
- package/registry/stateful-button.json +18 -0
- package/registry/text-animation/files/src/components/ui/text-animation.tsx +650 -0
- package/registry/text-animation/files/src/lib/kit.tsx +219 -0
- package/registry/text-animation.json +15 -0
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# @expokit/cli
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@expokit/cli)
|
|
4
|
+
[](https://github.com/Anshkaran7/expokit/actions/workflows/ci.yml)
|
|
5
|
+
[](https://scorecard.dev/viewer/?uri=github.com/Anshkaran7/expokit)
|
|
6
|
+

|
|
4
7
|

|
|
5
8
|
|
|
6
9
|
The CLI for **[ExpoKit](https://expokit.karandev.in)** — animated UI components for Expo and React Native. It copies
|
|
@@ -28,6 +31,17 @@ See every component: `npx @expokit/cli@latest add --list`
|
|
|
28
31
|
Everything else — live previews, props, installation, theming, Pro, AI / MCP setup and help — is on
|
|
29
32
|
**[expokit.karandev.in](https://expokit.karandev.in/docs)**.
|
|
30
33
|
|
|
34
|
+
## Security
|
|
35
|
+
|
|
36
|
+
- **No dependencies, no install scripts, no telemetry.**
|
|
37
|
+
- **Writes only inside your app** — `components.json`, the `@/*` alias in `tsconfig.json` and the component files.
|
|
38
|
+
`--dry-run` shows every change first; `add` never overwrites a file you changed unless you pass `--force`.
|
|
39
|
+
- **Free components need no network** — they ship inside this package. Pro components are downloaded over HTTPS from
|
|
40
|
+
expokit.karandev.in; your token is stored readable only by you, sent only to that site and never through a redirect.
|
|
41
|
+
- **Runs one command:** `npx expo install` for the packages a component needs.
|
|
42
|
+
|
|
43
|
+
Report a vulnerability privately: [SECURITY.md](https://github.com/Anshkaran7/expokit/blob/main/SECURITY.md).
|
|
44
|
+
|
|
31
45
|
## License
|
|
32
46
|
|
|
33
47
|
MIT
|
package/bin/expo-kit.js
CHANGED
|
@@ -13,6 +13,7 @@ import fs from 'node:fs';
|
|
|
13
13
|
import os from 'node:os';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import readline from 'node:readline';
|
|
16
|
+
import { Writable } from 'node:stream';
|
|
16
17
|
import { fileURLToPath } from 'node:url';
|
|
17
18
|
import {
|
|
18
19
|
CliError,
|
|
@@ -36,6 +37,8 @@ const REGISTRY_DIR = path.join(PACKAGE_ROOT, 'registry');
|
|
|
36
37
|
/** Website that serves Pro items (GET /api/registry/<name>) and issues CLI tokens (/account). */
|
|
37
38
|
const SITE_URL = (process.env.EXPO_KIT_URL || DEFAULT_SITE_URL).replace(/\/+$/, '');
|
|
38
39
|
const CREDENTIALS_DIR = path.join(os.homedir(), '.expo-kit');
|
|
40
|
+
/** How users run the CLI. Bare `expo-kit` only works once installed, and `npx expo-kit` would fetch an unrelated npm package. */
|
|
41
|
+
const RUN = 'npx @expokit/cli@latest';
|
|
39
42
|
const CREDENTIALS_PATH = path.join(CREDENTIALS_DIR, 'credentials.json');
|
|
40
43
|
|
|
41
44
|
const DEFAULT_ALIASES = {
|
|
@@ -295,7 +298,7 @@ async function loadRegistryItem(name) {
|
|
|
295
298
|
const resolved = resolveRegistryName(name);
|
|
296
299
|
const meta = isValidItemName(resolved) ? readMeta(resolved) : null;
|
|
297
300
|
if (!meta) {
|
|
298
|
-
if (!isValidItemName(resolved)) fail(`Unknown item "${name}". Run:
|
|
301
|
+
if (!isValidItemName(resolved)) fail(`Unknown item "${name}". Run: ${RUN} add --list`);
|
|
299
302
|
// Not in this package's registry — it may be newer than this CLI. Ask the site before giving up.
|
|
300
303
|
const remoteMeta = { name: resolved, title: resolved, tier: 'pro', dependencies: [], unknownLocally: true };
|
|
301
304
|
return stageRemoteItem(remoteMeta, resolved);
|
|
@@ -317,7 +320,7 @@ function listRegistry() {
|
|
|
317
320
|
console.log(` • ${meta.name.padEnd(18)} ${tag}${meta.title}`);
|
|
318
321
|
}
|
|
319
322
|
console.log('');
|
|
320
|
-
if (pro) log(`PRO items need an ExpoKit Pro plan — run
|
|
323
|
+
if (pro) log(`PRO items need an ExpoKit Pro plan — run \`${RUN} login\` (tokens: ${SITE_URL}/account).\n`);
|
|
321
324
|
}
|
|
322
325
|
|
|
323
326
|
// ---------------------------------------------------------------------------
|
|
@@ -342,9 +345,9 @@ function assertTokenTarget(creds) {
|
|
|
342
345
|
const target = new URL(SITE_URL).origin;
|
|
343
346
|
throw new CliError(
|
|
344
347
|
creds.fromEnv
|
|
345
|
-
? `Refusing to send EXPO_KIT_TOKEN to ${target} (only ${DEFAULT_SITE_URL} or localhost receive it). Unset EXPO_KIT_URL, or run
|
|
348
|
+
? `Refusing to send EXPO_KIT_TOKEN to ${target} (only ${DEFAULT_SITE_URL} or localhost receive it). Unset EXPO_KIT_URL, or run \`${RUN} login\` against that site.`
|
|
346
349
|
: `Your saved token was created for ${creds.url ?? DEFAULT_SITE_URL}, so it won't be sent to ${target}. ` +
|
|
347
|
-
`Unset EXPO_KIT_URL, or run
|
|
350
|
+
`Unset EXPO_KIT_URL, or run \`${RUN} login\` with EXPO_KIT_URL=${target} to save a token for that site.`
|
|
348
351
|
);
|
|
349
352
|
}
|
|
350
353
|
|
|
@@ -361,6 +364,8 @@ async function apiGet(pathname, token) {
|
|
|
361
364
|
try {
|
|
362
365
|
res = await fetch(url, {
|
|
363
366
|
headers: { Accept: 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
|
|
367
|
+
// A request that carries the token never follows a redirect: the token goes to SITE_URL's origin only.
|
|
368
|
+
redirect: token ? 'manual' : 'follow',
|
|
364
369
|
signal: AbortSignal.timeout(20000),
|
|
365
370
|
});
|
|
366
371
|
} catch (e) {
|
|
@@ -369,6 +374,14 @@ async function apiGet(pathname, token) {
|
|
|
369
374
|
'Check your connection (or EXPO_KIT_URL) and try again.'
|
|
370
375
|
);
|
|
371
376
|
}
|
|
377
|
+
if (token && res.status >= 300 && res.status < 400) {
|
|
378
|
+
const location = res.headers.get('location');
|
|
379
|
+
fail(
|
|
380
|
+
`${SITE_URL} answered with a redirect (HTTP ${res.status}${location ? ` to ${location}` : ''}). ` +
|
|
381
|
+
`Your token is only sent to ${new URL(SITE_URL).origin} directly, so nothing was sent on. ` +
|
|
382
|
+
'Check EXPO_KIT_URL, or update the CLI: npx @expokit/cli@latest'
|
|
383
|
+
);
|
|
384
|
+
}
|
|
372
385
|
let body = null;
|
|
373
386
|
try {
|
|
374
387
|
body = await res.json();
|
|
@@ -388,7 +401,7 @@ async function fetchRemoteItem(name, { unknownLocally = false } = {}) {
|
|
|
388
401
|
assertTokenTarget(creds);
|
|
389
402
|
const { status, ok, body } = await apiGet(`/api/registry/${encodeURIComponent(name)}`, creds?.token);
|
|
390
403
|
if (ok && body && Array.isArray(body.files)) return body;
|
|
391
|
-
if (status === 404 && unknownLocally) fail(`Unknown item "${name}". Run:
|
|
404
|
+
if (status === 404 && unknownLocally) fail(`Unknown item "${name}". Run: ${RUN} add --list`);
|
|
392
405
|
if (status >= 500) failServerError(status, `downloading "${name}"`);
|
|
393
406
|
if (status === 401 && !creds) {
|
|
394
407
|
fail(
|
|
@@ -398,7 +411,7 @@ async function fetchRemoteItem(name, { unknownLocally = false } = {}) {
|
|
|
398
411
|
);
|
|
399
412
|
}
|
|
400
413
|
if (status === 401) {
|
|
401
|
-
fail(`Your token (${creds.source}) is invalid or was revoked. Create a new one at ${SITE_URL}/account and run
|
|
414
|
+
fail(`Your token (${creds.source}) is invalid or was revoked. Create a new one at ${SITE_URL}/account and run \`${RUN} login\`.`);
|
|
402
415
|
}
|
|
403
416
|
if (status === 403) {
|
|
404
417
|
fail(`"${name}" needs ExpoKit Pro, and your account doesn't have it yet. Upgrade at ${SITE_URL}/pricing.`);
|
|
@@ -484,11 +497,20 @@ async function stageRemoteItem(meta, resolved) {
|
|
|
484
497
|
return { meta: { ...meta, title, dependencies, sharedFiles: [...shared] }, filesRoot: tmp, resolved };
|
|
485
498
|
}
|
|
486
499
|
|
|
487
|
-
|
|
488
|
-
|
|
500
|
+
/** Reads one line from the terminal without echoing it (for tokens), so it stays off the screen and out of scrollback. */
|
|
501
|
+
function promptSecret(question) {
|
|
502
|
+
process.stdout.write(question);
|
|
503
|
+
const muted = new Writable({ write: (_chunk, _encoding, done) => done() });
|
|
504
|
+
const rl = readline.createInterface({ input: process.stdin, output: muted, terminal: true });
|
|
489
505
|
return new Promise((resolve) => {
|
|
490
|
-
rl.
|
|
506
|
+
rl.on('SIGINT', () => {
|
|
507
|
+
rl.close();
|
|
508
|
+
process.stdout.write('\n');
|
|
509
|
+
process.exit(130);
|
|
510
|
+
});
|
|
511
|
+
rl.question('', (answer) => {
|
|
491
512
|
rl.close();
|
|
513
|
+
process.stdout.write('\n');
|
|
492
514
|
resolve(answer.trim());
|
|
493
515
|
});
|
|
494
516
|
});
|
|
@@ -498,9 +520,9 @@ async function cmdLogin(args) {
|
|
|
498
520
|
const i = args.findIndex((a) => a === '--token' || a.startsWith('--token='));
|
|
499
521
|
let token = i === -1 ? '' : args[i].includes('=') ? args[i].slice('--token='.length) : args[i + 1] ?? '';
|
|
500
522
|
if (!token) {
|
|
501
|
-
log(`Create a CLI token at ${SITE_URL}/account (signed in), then paste it here.`);
|
|
502
|
-
if (!process.stdin.isTTY) fail(
|
|
503
|
-
token = await
|
|
523
|
+
log(`Create a CLI token at ${SITE_URL}/account (signed in), then paste it here (it won't be shown).`);
|
|
524
|
+
if (!process.stdin.isTTY) fail(`No token given. Use: ${RUN} login --token <token>`);
|
|
525
|
+
token = await promptSecret('Token: ');
|
|
504
526
|
}
|
|
505
527
|
token = token.trim();
|
|
506
528
|
if (!token) fail('No token given.');
|
|
@@ -532,7 +554,7 @@ function cmdLogout() {
|
|
|
532
554
|
|
|
533
555
|
async function cmdWhoami() {
|
|
534
556
|
const creds = readCredentials();
|
|
535
|
-
if (!creds) fail(`Not logged in. Run:
|
|
557
|
+
if (!creds) fail(`Not logged in. Run: ${RUN} login (tokens: ${SITE_URL}/account)`);
|
|
536
558
|
assertTokenTarget(creds);
|
|
537
559
|
const { status, ok, body } = await apiGet('/api/cli/whoami', creds.token);
|
|
538
560
|
if (status === 401) fail(`Your token (${creds.source}) is invalid or was revoked. Create a new one at ${SITE_URL}/account.`);
|
|
@@ -740,9 +762,43 @@ function runExpoInstall(projectRoot, packages, dryRun) {
|
|
|
740
762
|
return true;
|
|
741
763
|
}
|
|
742
764
|
|
|
743
|
-
|
|
765
|
+
/** Folder names `init <dir>` will create: no paths, no spaces, nothing a shell could misread. */
|
|
766
|
+
const SAFE_APP_DIR = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
767
|
+
|
|
768
|
+
/** `init my-app`: a new Expo app (create-expo-app) in ./my-app, then the usual init inside it. */
|
|
769
|
+
function createExpoApp(cwd, dir, flags) {
|
|
770
|
+
if (!SAFE_APP_DIR.test(dir)) fail(`"${dir}" is not a valid folder name (letters, numbers, ".", "_" and "-").`);
|
|
771
|
+
const target = path.join(cwd, dir);
|
|
772
|
+
if (fs.existsSync(target) && fs.readdirSync(target).length) fail(`./${dir} already exists and is not empty.`);
|
|
773
|
+
if (flags.dryRun) {
|
|
774
|
+
log(`Would run: npx create-expo-app@latest ${dir} --yes`);
|
|
775
|
+
log(`Would then set up ExpoKit in ./${dir}`);
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
log(`Creating a new Expo app in ./${dir}…`);
|
|
779
|
+
const result = spawnSync('npx', ['create-expo-app@latest', dir, '--yes'], {
|
|
780
|
+
cwd,
|
|
781
|
+
stdio: 'inherit',
|
|
782
|
+
shell: process.platform === 'win32',
|
|
783
|
+
});
|
|
784
|
+
if (result.status !== 0 || !fs.existsSync(path.join(target, 'package.json'))) {
|
|
785
|
+
fail(`create-expo-app failed. Create the app yourself (npx create-expo-app@latest ${dir}), then run ${RUN} init inside it.`);
|
|
786
|
+
}
|
|
787
|
+
return target;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function cmdInit(cwd, flags, dir) {
|
|
791
|
+
if (dir) {
|
|
792
|
+
// Already inside an Expo app: `init <dir>` still means "create a new one there".
|
|
793
|
+
const target = createExpoApp(cwd, dir, flags);
|
|
794
|
+
if (!target) return;
|
|
795
|
+
cmdInit(target, flags);
|
|
796
|
+
log('');
|
|
797
|
+
log(`Next: cd ${dir} && npx expo start — then scan the QR with Expo Go.`);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
744
800
|
const projectRoot = findProjectRoot(cwd);
|
|
745
|
-
if (!projectRoot) fail(
|
|
801
|
+
if (!projectRoot) fail(`Not inside an Expo project (expo dependency not found). Start a new app with: ${RUN} init my-app`);
|
|
746
802
|
|
|
747
803
|
const configPath = path.join(projectRoot, 'components.json');
|
|
748
804
|
if (fs.existsSync(configPath)) {
|
|
@@ -777,7 +833,7 @@ function cmdInit(cwd, flags) {
|
|
|
777
833
|
log(`Created components.json (${layout} layout)`);
|
|
778
834
|
|
|
779
835
|
ensureExpoRouterMain(projectRoot, false);
|
|
780
|
-
log(
|
|
836
|
+
log(`Done. Add items with: ${RUN} add <name>`);
|
|
781
837
|
}
|
|
782
838
|
|
|
783
839
|
async function cmdAdd(args, cwd) {
|
|
@@ -891,7 +947,7 @@ async function cmdAdd(args, cwd) {
|
|
|
891
947
|
if (resolved === 'liquidglass-tab') {
|
|
892
948
|
log('Next steps:');
|
|
893
949
|
console.log(' • Edit src/lib/tabs.ts — labels, icon presets, colors, screen text');
|
|
894
|
-
console.log(
|
|
950
|
+
console.log(` • Run: ${RUN} doctor`);
|
|
895
951
|
console.log(' • Build a dev client: npx expo run:ios (iOS 26 shows real Liquid Glass,');
|
|
896
952
|
console.log(' iOS 18 and older, Android and web get the glass fallback bar)');
|
|
897
953
|
console.log(' • After package updates: npx @expokit/cli@latest update');
|
|
@@ -977,7 +1033,7 @@ function cmdDoctor(cwd) {
|
|
|
977
1033
|
log('Checking project…\n');
|
|
978
1034
|
|
|
979
1035
|
if (!fs.existsSync(config.configPath)) {
|
|
980
|
-
warn(
|
|
1036
|
+
warn(`Missing components.json — run: ${RUN} init`);
|
|
981
1037
|
issues += 1;
|
|
982
1038
|
} else {
|
|
983
1039
|
log(' ✓ components.json');
|
|
@@ -996,7 +1052,7 @@ function cmdDoctor(cwd) {
|
|
|
996
1052
|
const usesLiquidGlass = fs.existsSync(tabsPath) || config.installed?.['liquidglass-tab'];
|
|
997
1053
|
if (usesLiquidGlass) {
|
|
998
1054
|
if (!fs.existsSync(tabsPath)) {
|
|
999
|
-
warn(
|
|
1055
|
+
warn(`No src/lib/tabs.ts — run: ${RUN} add liquidglass-tab`);
|
|
1000
1056
|
issues += 1;
|
|
1001
1057
|
} else {
|
|
1002
1058
|
log(' ✓ src/lib/tabs.ts');
|
|
@@ -1018,7 +1074,7 @@ function cmdDoctor(cwd) {
|
|
|
1018
1074
|
const routePath = path.join(tabsDir, fileName);
|
|
1019
1075
|
if (!fs.existsSync(routePath)) {
|
|
1020
1076
|
warn(`Missing route for tab "${tabName}": ${path.relative(projectRoot, routePath)}`);
|
|
1021
|
-
console.log(` Fix: add the file or run
|
|
1077
|
+
console.log(` Fix: add the file or run ${RUN} add liquidglass-tab --force`);
|
|
1022
1078
|
issues += 1;
|
|
1023
1079
|
} else {
|
|
1024
1080
|
log(` ✓ route ${path.relative(projectRoot, routePath)}`);
|
|
@@ -1073,7 +1129,7 @@ async function cmdMcp(args, cwd) {
|
|
|
1073
1129
|
const i = args.findIndex((a) => a === '--client' || a.startsWith('--client='));
|
|
1074
1130
|
const client = i < 0 ? undefined : args[i].includes('=') ? args[i].split('=')[1] : args[i + 1];
|
|
1075
1131
|
const names = [...Object.keys(MCP_CLIENTS), 'codex'].join('|');
|
|
1076
|
-
if (!client) fail(`Pick a client:
|
|
1132
|
+
if (!client) fail(`Pick a client: ${RUN} mcp init --client <${names}>`);
|
|
1077
1133
|
|
|
1078
1134
|
if (client === 'codex') {
|
|
1079
1135
|
log('Codex reads MCP servers from ~/.codex/config.toml (shared by every project). Add:\n');
|
|
@@ -1117,7 +1173,7 @@ function printHelp() {
|
|
|
1117
1173
|
expo-kit — copy-paste Expo UI (shadcn-style)
|
|
1118
1174
|
|
|
1119
1175
|
Usage:
|
|
1120
|
-
npx @expokit/cli@latest init
|
|
1176
|
+
npx @expokit/cli@latest init [my-app] [--dry-run]
|
|
1121
1177
|
npx @expokit/cli@latest add <name...> [--dry-run] [--force]
|
|
1122
1178
|
npx @expokit/cli@latest update [name] [--dry-run] [--force]
|
|
1123
1179
|
npx @expokit/cli@latest doctor
|
|
@@ -1129,6 +1185,7 @@ Usage:
|
|
|
1129
1185
|
npx @expokit/cli@latest mcp init --client <claude|cursor|vscode|codex>
|
|
1130
1186
|
|
|
1131
1187
|
Examples:
|
|
1188
|
+
npx @expokit/cli@latest init my-app new Expo app with ExpoKit set up
|
|
1132
1189
|
npx @expokit/cli@latest add liquidglass-tab --dry-run
|
|
1133
1190
|
npx @expokit/cli@latest add liquidglass-tab --force
|
|
1134
1191
|
npx @expokit/cli@latest update
|
|
@@ -1159,10 +1216,15 @@ Config: components.json (created by init)
|
|
|
1159
1216
|
async function main(command, rest) {
|
|
1160
1217
|
switch (command) {
|
|
1161
1218
|
case 'init': {
|
|
1162
|
-
const { flags } = parseFlags(rest);
|
|
1163
|
-
|
|
1219
|
+
const { flags, positional } = parseFlags(rest);
|
|
1220
|
+
if (positional.length > 1) fail(`init takes one folder name: ${RUN} init my-app`);
|
|
1221
|
+
cmdInit(process.cwd(), flags, positional[0]);
|
|
1164
1222
|
break;
|
|
1165
1223
|
}
|
|
1224
|
+
case '--version':
|
|
1225
|
+
case '-v':
|
|
1226
|
+
console.log(getPackageVersion());
|
|
1227
|
+
break;
|
|
1166
1228
|
case 'add':
|
|
1167
1229
|
await cmdAdd(rest, process.cwd());
|
|
1168
1230
|
break;
|
package/bin/mcp.mjs
CHANGED
|
@@ -179,7 +179,7 @@ function searchItems(catalog, { query }) {
|
|
|
179
179
|
function viewItem(catalog, registryDir, { name, include_source: includeSource = false }) {
|
|
180
180
|
const it = requireItem(catalog, name);
|
|
181
181
|
const doc = catalog.docs.get(it.name);
|
|
182
|
-
const out = [`# ${it.title} (${it.name})`, '', it.description, ''];
|
|
182
|
+
const out = [`# ${it.title} (${it.name})`, '', ...(doc?.summary ? [doc.summary, ''] : []), it.description, ''];
|
|
183
183
|
out.push(`Type: ${it.type} · Tier: ${it.tier === 'pro' ? 'PRO (needs an ExpoKit Pro plan)' : 'free'}${doc?.category ? ` · Category: ${doc.category}` : ''}`);
|
|
184
184
|
if (doc?.aliases?.length) out.push(`Aliases: ${doc.aliases.join(', ')}`);
|
|
185
185
|
out.push('', `Add: \`${addCommand([it.name])}\``);
|
|
@@ -195,6 +195,13 @@ function viewItem(catalog, registryDir, { name, include_source: includeSource =
|
|
|
195
195
|
for (const [prop, type, def, desc] of doc.props) out.push(`| ${cell(prop)} | ${cell(type)} | ${cell(def)} | ${cell(desc)} |`);
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
if (doc?.behavior?.length) {
|
|
199
|
+
out.push('', '## Behaviour & accessibility', '');
|
|
200
|
+
for (const [label, text] of doc.behavior) out.push(`- **${label}:** ${text}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (doc?.usage) out.push('', '## Usage', '', '```tsx', doc.usage.trimEnd(), '```');
|
|
204
|
+
|
|
198
205
|
if (doc?.examples?.length) {
|
|
199
206
|
out.push('', '## Examples');
|
|
200
207
|
for (const ex of doc.examples) out.push('', `### ${ex.title}`, '', '```tsx', ex.code.trimEnd(), '```');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expokit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -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",
|
|
@@ -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,6 +159,29 @@
|
|
|
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",
|
|
@@ -6,6 +6,7 @@
|
|
|
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
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"src/components/ui/bottom-nav.tsx"
|
|
12
12
|
],
|
|
13
|
+
"summary": "A tab bar whose indicator stretches between tabs, leading edge first.",
|
|
13
14
|
"props": [
|
|
14
15
|
[
|
|
15
16
|
"items",
|
|
@@ -84,6 +85,29 @@
|
|
|
84
85
|
"Style of the bar (the outer shadow view for `floating`)."
|
|
85
86
|
]
|
|
86
87
|
],
|
|
88
|
+
"behavior": [
|
|
89
|
+
[
|
|
90
|
+
"Touch",
|
|
91
|
+
"Tap a tab to select it. Items marked action call onAction instead and never become the active tab."
|
|
92
|
+
],
|
|
93
|
+
[
|
|
94
|
+
"Motion",
|
|
95
|
+
"The indicator springs its leading edge first and the trailing edge catches up. Icons morph from outline to filled and badges pop in."
|
|
96
|
+
],
|
|
97
|
+
[
|
|
98
|
+
"Reduced motion",
|
|
99
|
+
"The indicator glides with a short timing and no stretch, and icon changes use quick timings instead of springs."
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
"Haptics",
|
|
103
|
+
"A selection tick on tab change and a light impact on the action button. Turn off with haptics={false}."
|
|
104
|
+
],
|
|
105
|
+
[
|
|
106
|
+
"Screen readers",
|
|
107
|
+
"The bar is a tablist and each tab carries selected state, with the badge count in its label. Action items are buttons."
|
|
108
|
+
]
|
|
109
|
+
],
|
|
110
|
+
"usage": "import { useState } from 'react';\nimport { BottomNav } from '@/components/ui/bottom-nav';\n\nconst [tab, setTab] = useState('home');\n<BottomNav\n items={[{ key: 'home', label: 'Home', icon: 'home' }, { key: 'inbox', label: 'Inbox', icon: 'inbox', badge: 3 }]}\n value={tab}\n onValueChange={setTab}\n/>",
|
|
87
111
|
"examples": [
|
|
88
112
|
{
|
|
89
113
|
"name": "MinimalClean",
|