diff --git a/.github/workflows/skill-routing-coverage.yml b/.github/workflows/skill-routing-coverage.yml new file mode 100644 index 0000000..efb3b0e --- /dev/null +++ b/.github/workflows/skill-routing-coverage.yml @@ -0,0 +1,84 @@ +name: skill routing coverage + +on: + pull_request: + branches: + - main + merge_group: + branches: + - main + +concurrency: + group: skill-routing-coverage-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + issues: write + +jobs: + check: + name: check expo-overview routing + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + + - name: Check expo-overview routing coverage + id: routing-check + continue-on-error: true + env: + ROUTING_CHECK_SUMMARY_PATH: .context/skill-routing-check-summary.md + run: bun scripts/check-overview-routing.ts + + - name: Add workflow summary + if: always() && hashFiles('.context/skill-routing-check-summary.md') != '' + run: cat .context/skill-routing-check-summary.md >> "$GITHUB_STEP_SUMMARY" + + - name: Comment on pull request + if: always() && github.event_name == 'pull_request' && hashFiles('.context/skill-routing-check-summary.md') != '' + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const fs = require("node:fs"); + + const marker = ""; + const summary = fs.readFileSync(".context/skill-routing-check-summary.md", "utf8"); + const body = `${marker}\n${summary}`; + const issue_number = context.payload.pull_request.number; + const { owner, repo } = context.repo; + + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number, + per_page: 100, + }); + const existingComment = comments.find( + (comment) => comment.user?.type === "Bot" && comment.body?.includes(marker) + ); + + if (existingComment) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existingComment.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body, + }); + } + + - name: Fail on missing routing + if: steps.routing-check.outcome == 'failure' + run: exit 1 diff --git a/AGENTS.md b/AGENTS.md index 04e596e..e33a5db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -191,8 +191,9 @@ When changing Claude Code marketplace aliases, preserve backward compatibility u 1. Create `plugins/expo/skills/my-skill/SKILL.md`. 2. Add focused reference files under `plugins/expo/skills/my-skill/references/` when the skill needs more detail than belongs in the main `SKILL.md`. 3. Add scripts under `plugins/expo/skills/my-skill/scripts/` only for reusable logic. -4. Update `plugins/expo/README.md` or the root `README.md` only when the user-facing installation or usage story changes. -5. Keep the skill under the existing `expo` plugin unless there is a clear distribution reason to create a new plugin. +4. Add a one-line entry for the skill to the `expo-overview` Skill Map (`plugins/expo/skills/expo-overview/SKILL.md`) so the router can dispatch to it. This is enforced by the `skill routing coverage` workflow (`bun scripts/check-overview-routing.ts`). +5. Update `plugins/expo/README.md` or the root `README.md` only when the user-facing installation or usage story changes. +6. Keep the skill under the existing `expo` plugin unless there is a clear distribution reason to create a new plugin. ## Testing Plugins diff --git a/plugins/expo/README.md b/plugins/expo/README.md index 8e071b4..fa4ff6c 100644 --- a/plugins/expo/README.md +++ b/plugins/expo/README.md @@ -62,6 +62,10 @@ Official AI agent skills from the Expo team for building, deploying, upgrading, ## Skills Included +### Start Here + +- **expo-overview** — Router and shared setup rules; the entry point when a request is vague or the user hasn't named a specific Expo tool + ### App Design - **add-app-clip** — Add an iOS App Clip target to an Expo app (AASA, associated domains, TestFlight) @@ -69,6 +73,8 @@ Official AI agent skills from the Expo team for building, deploying, upgrading, - **expo-api-routes** — Create API routes in Expo Router with EAS Hosting - **expo-brownfield** — Integrate Expo and React Native into existing native iOS or Android apps - **expo-dev-client** — Build and distribute Expo development clients locally or via TestFlight +- **expo-examples** — Canonical, version-matched patterns from the official expo/examples repo (Stripe, Clerk, Supabase, and more) +- **expo-module** — Write Expo native modules and views with the Expo Modules API (Swift, Kotlin, TypeScript) - **expo-tailwind-setup** — Set up Tailwind CSS v4 in Expo with NativeWind v5 - **expo-ui** — Native UI with @expo/ui: universal cross-platform components first, with SwiftUI and Jetpack Compose for platform-specific needs - **native-data-fetching** — Network requests, API calls, caching, and offline support @@ -79,6 +85,7 @@ Official AI agent skills from the Expo team for building, deploying, upgrading, - **eas-update-insights** — Check EAS Update health, crash rates, adoption, and payload size - **expo-deployment** — Deploy to iOS App Store, Android Play Store, and web hosting - **expo-cicd-workflows** — EAS workflow YAML files for CI/CD pipelines +- **expo-observe** — Measure startup, launch, and TTI performance with EAS Observe ### Upgrading diff --git a/plugins/expo/skills/building-native-ui/SKILL.md b/plugins/expo/skills/building-native-ui/SKILL.md index 4d302ca..4fd2b90 100644 --- a/plugins/expo/skills/building-native-ui/SKILL.md +++ b/plugins/expo/skills/building-native-ui/SKILL.md @@ -7,6 +7,8 @@ license: MIT # Expo UI Guidelines +> **Before picking any UI component, check `expo-ui` first.** `@expo/ui` provides native equivalents — BottomSheet, Button, Picker, Slider, Menu, Section, Switch, SegmentedControl, and more — rendered as real SwiftUI on iOS and Jetpack Compose on Android, available in Expo Go on SDK 56+ with no custom build. Load the **`expo-ui`** skill to find the right component before falling back to React Native built-ins or community libraries. This skill (`building-native-ui`) covers the surrounding structure: Expo Router navigation, layout, styling, and animations. + ## References Consult these resources as needed: @@ -78,6 +80,7 @@ See `./references/route-structure.md` for detailed route conventions. ## Library Preferences +- **For any sheet, picker, slider, toggle, menu, or grouped-form section: use `@expo/ui` (see `expo-ui` skill) before reaching for a React Native built-in or community library** — it renders native SwiftUI/Compose and works in Expo Go on SDK 56+. For grouped/settings-style rows (short, fixed-length), use `@expo/ui`'s `List` + `ListItem`. For large or unknown-length scrolling lists (feeds, search results, catalogs), use `FlatList` or `FlashList` — `@expo/ui`'s `List` is not virtualized. - Never use modules removed from React Native such as Picker, WebView, SafeAreaView, or AsyncStorage - Never use legacy expo-permissions - `expo-audio` not `expo-av` @@ -105,7 +108,7 @@ See `./references/route-structure.md` for detailed route conventions. - Use views with built-in haptics like `` from React Native and `@react-native-community/datetimepicker` - When a route belongs to a Stack, its first child should almost always be a ScrollView with `contentInsetAdjustmentBehavior="automatic"` set - When adding a `ScrollView` to the page it should almost always be the first component inside the route component -- Prefer `headerSearchBarOptions` in Stack.Screen options to add a search bar +- Search bars belong in the Stack header via `headerSearchBarOptions` — **never** as a floating `View` pinned above content or above the tab bar. See `./references/search.md`. - Use the `` prop on text containing data that could be copied - Consider formatting large numbers like 1.4M or 38k - Never use intrinsic elements like 'img' or 'div' unless in a webview or Expo DOM component diff --git a/plugins/expo/skills/building-native-ui/references/controls.md b/plugins/expo/skills/building-native-ui/references/controls.md index 762fe20..0b0257d 100644 --- a/plugins/expo/skills/building-native-ui/references/controls.md +++ b/plugins/expo/skills/building-native-ui/references/controls.md @@ -2,6 +2,8 @@ Native iOS controls provide built-in haptics, accessibility, and platform-appropriate styling. +> **Prefer `@expo/ui` for these controls.** Its `Slider`, `Switch`, `DateTimePicker`, `Menu`, and segmented pickers render as real SwiftUI / Jetpack Compose and follow platform design conventions automatically — recommended over the React Native controls below. See the **`expo-ui`** skill; use the React Native controls below as a fallback when you're not using `@expo/ui`. + ## Switch Use for binary on/off settings. Has built-in haptics. diff --git a/plugins/expo/skills/expo-overview/SKILL.md b/plugins/expo/skills/expo-overview/SKILL.md new file mode 100644 index 0000000..b723cbc --- /dev/null +++ b/plugins/expo/skills/expo-overview/SKILL.md @@ -0,0 +1,95 @@ +--- +name: expo-overview +description: > + Entry point and router for any Expo or EAS task — load this skill first, before writing + any code, whenever the task involves building, extending, or modifying a React Native / + Expo app. Triggers on: any multi-screen app spec or design to implement (tabs, stacks, + maps, lists, navigation); building from a screenshot or design reference; adding screens + or features to an existing Expo project; or when the request mentions Expo, EAS, React + Native, or any expo-* package. Handles phrasings like "implement a mobile app", "build + an app from this screenshot", "create an app that looks like this design", "make my app + look native", "build a beautiful app", "align with iOS / SwiftUI best practices", "build + with native iOS patterns", "add navigation", "fetch some data", "upgrade my SDK", "add + Expo to my existing native app", "ship to the App Store", "push a fix without + resubmitting", or "I'm new to Expo, where do I start". Detects the real goal, routes to + the right expo-* / eas-* skill, and owns the shared environment and setup rules the + other Expo skills rely on. +version: 1.0.0 +license: MIT +--- + +# `expo-overview` — router & shared rules for Expo / EAS + +## Start Here — read before doing anything + +**Do not guess the skill from project files alone.** Many Expo goals look similar from +the filesystem but need different skills. + +1. **Read the user's goal** — what outcome do they want, in plain terms? +2. **Classify it** using the Skill Map below, translating casual phrasing to a goal. +3. **Confirm intent** if ambiguous ("Sounds like you want to ship to the stores — that's + `expo-deployment`. Right?"), then load that skill's `SKILL.md` and follow it. +4. **Trust the leaf skill** — it has its own detection logic and steps. Don't improvise. + +## Skill Map (by goal) + +Match the goal to a category, then the skill, then load that leaf's `SKILL.md`. + +**Build the app** +- `building-native-ui` — screens, navigation (tabs / stacks / modals), styling, animations, layout +- `expo-ui` — native UI components via `@expo/ui`: BottomSheet, Picker, Slider, Button, Menu, Section, and more — real SwiftUI on iOS, Jetpack Compose on Android, available in Expo Go on SDK 56+; also drop-in replacements for `@gorhom/bottom-sheet`, `datetimepicker`, etc. +- `expo-tailwind-setup` — Tailwind / NativeWind styling +- `native-data-fetching` — network requests, React Query / SWR, caching, offline, route loaders +- `expo-api-routes` — server endpoints / API routes with EAS Hosting +- `use-dom` — run web code or reuse a web library inside native + +> **Component selection rule:** whenever you need a UI component (list rows, bottom sheets, pickers, sliders, menus, buttons, segmented controls, toggles), **consult `expo-ui` first** to check whether `@expo/ui` has a native equivalent before reaching for a React Native built-in or a community library. Native `@expo/ui` components give the best platform fit with zero extra install steps on SDK 56+. Load `expo-ui` alongside `building-native-ui` for any app that renders lists, detail sheets, or form controls. + +**Ship & operate** +- `expo-deployment` — build, submit to the App Store / Play Store / TestFlight, and push OTA updates +- `expo-cicd-workflows` — EAS Workflow YAML and CI/CD pipelines +- `expo-dev-client` — custom development builds +- `eas-update-insights` — OTA update health: crash rate, adoption, payload size +- `expo-observe` — startup / launch / TTI performance with EAS Observe + +**Extend natively** +- `expo-module` — native modules and views (Swift / Kotlin) with the Expo Modules API +- `expo-brownfield` — embed Expo / React Native in an existing native app +- `add-app-clip` — iOS App Clip target (AASA, smart app banner) + +**Maintain & learn** +- `upgrading-expo` — upgrade the Expo SDK and fix dependency conflicts +- `expo-examples` — canonical, version-matched integration examples (Stripe, Clerk, Supabase, …) + +### Translating vague asks + +Some everyday phrasings don't obviously map to a skill name — translate before routing: + +- "Make it look native" → grouped controls / settings forms = `expo-ui`; screens, navigation, animations = `building-native-ui`. +- "Ship it" / "get an .ipa or .apk" / "push a fix without resubmitting" → all `expo-deployment` (it owns the whole release path, including OTA updates). +- "I'm new / where do I start" → scaffold first (see First Run), then route by goal. + +## First Run / shared rules + +These apply across every Expo skill, so handle them here once instead of repeating them +in each leaf. + +- **No Expo project yet?** Start one the standard way before routing to a feature skill: + `npx create-expo-app@latest`. Then classify the user's goal and route. +- **Detect the SDK version** before giving version-specific advice: read the `expo` + version in `package.json` (and `app.json` / `app.config.{js,ts}`). Many APIs and + defaults differ by SDK. +- **Managed vs. bare/prebuild**: the presence of committed `ios/` and `android/` + directories means native projects exist (prebuild or bare). Config-plugin and + native-setup steps differ — note which one the project is in. +- **Install packages with `npx expo install `**, not raw `npm`/`yarn`/`pnpm add`, + so versions stay compatible with the project's SDK. +- **EAS auth & linking** (only needed for build/submit/update/observe/workflows): check + login with `eas whoami`, log in with `eas login`. A project is linked when + `extra.eas.projectId` exists in the app config; create it with `eas init` if missing. + +## When NOT to use this skill + +- The user already named a specific Expo workflow or tool → go straight to that skill. +- A more specific `expo-*` / `eas-*` skill obviously fits the request → use it and skip + the router hop. diff --git a/plugins/expo/skills/expo-ui/SKILL.md b/plugins/expo/skills/expo-ui/SKILL.md index fd59c8c..517b894 100644 --- a/plugins/expo/skills/expo-ui/SKILL.md +++ b/plugins/expo/skills/expo-ui/SKILL.md @@ -1,6 +1,6 @@ --- name: expo-ui -description: "Build native UI with the @expo/ui package: real SwiftUI on iOS and Jetpack Compose on Android rendered from React in an Expo or React Native app. Covers universal cross-platform components (Host, Column, Row, Button, Text, List, and more imported from @expo/ui), drop-in replacements for popular React Native community libraries (BottomSheet, DateTimePicker, Slider, Menu, etc.), and platform-specific SwiftUI (@expo/ui/swift-ui, iOS only) and Jetpack Compose (@expo/ui/jetpack-compose, Android only) trees and modifiers. Use when adding or reviewing @expo/ui Host/RNHostView trees, building native-feeling UI where standard React Native components fall short (grouped settings forms with toggles, sections, menus, sheets, pickers, sliders), choosing between universal and platform-specific components, or replacing an RN community UI library with a native @expo/ui equivalent. Not for custom native modules, Expo Router navigation, Reanimated, or data fetching." +description: "Build native UI with the @expo/ui package: real SwiftUI on iOS and Jetpack Compose on Android. Default to @expo/ui for sheets (BottomSheet), pickers, sliders, toggles, menus, and grouped-form sections — do NOT reach for Reanimated, @gorhom/bottom-sheet, or RN built-in Picker/Switch; use @expo/ui instead. Fall back to RN built-ins only when @expo/ui is missing the component. NOTE: @expo/ui List renders native grouped rows like an iOS Settings screen — it is NOT a virtualized list; use FlatList/FlashList for large datasets. Covers universal components (Host, Column, Row, Button, Text, List, BottomSheet, FieldGroup, Switch, Slider, Picker, Menu), drop-in replacements for RN community libraries, and platform-specific SwiftUI/Jetpack Compose trees. Not for Expo Router navigation, Reanimated, or data fetching." version: 1.0.0 license: MIT allowed-tools: "Bash(node *expo-ui/scripts/list-components.js *)" @@ -8,9 +8,9 @@ allowed-tools: "Bash(node *expo-ui/scripts/list-components.js *)" # Expo UI (`@expo/ui`) -`@expo/ui` renders real native UI from React: SwiftUI on iOS, Jetpack Compose on Android. Start with its universal components (one tree for iOS, Android, and web) and drop to platform-specific SwiftUI/Jetpack Compose only when the universal layer falls short. It also ships drop-in replacements for migrating off RN community UI libraries. +`@expo/ui` renders real native UI from React: SwiftUI on iOS, Jetpack Compose on Android. It also ships drop-in replacements for migrating off RN community UI libraries. -> These instructions track the latest Expo SDK. The **universal** layer requires **SDK 56+**. Drop-in replacements and the platform-specific layers also exist on SDK 55. For component details on a specific SDK, refer to the Expo UI docs for that version. +> These instructions track the latest Expo SDK. The **universal** layer requires **SDK 56+** and works in Expo Go — no custom build needed. Drop-in replacements and the platform-specific layers also exist on SDK 55. For component details on a specific SDK, refer to the Expo UI docs for that version. ## Installation @@ -18,11 +18,57 @@ allowed-tools: "Bash(node *expo-ui/scripts/list-components.js *)" npx expo install @expo/ui ``` -On SDK 56, `@expo/ui` works in Expo Go, so `npx expo start` runs it directly — no custom build required. On older SDKs, build a dev client first (`npx expo run:ios` / `npx expo run:android`). - Every `@expo/ui` tree — universal or platform-specific — must be wrapped in `Host`. -## Choosing an approach (read this first) +## Use @expo/ui by default — don't reach for RN alternatives first + +**Before using Reanimated, `@gorhom/bottom-sheet`, React Native's built-in `Switch`/`Picker`, or any community UI library for the items below, use `@expo/ui` instead.** Only fall back to RN built-ins when `@expo/ui` is missing the component. + +| Need | Use | +|------|-----| +| Slide-up sheet / bottom sheet | `BottomSheet` from `@expo/ui` — **not** Reanimated or `@gorhom/bottom-sheet` | +| Grouped native list rows (settings/form-style) | `List` + `ListItem` from `@expo/ui` — **not** `FlatList` (see note below) | +| Toggle | `Switch` from `@expo/ui` | +| Slider | `Slider` from `@expo/ui` | +| Date/time picker | `@expo/ui/community/datetimepicker` | +| Menu | `Menu` from `@expo/ui` | +| Form section with label | `FieldGroup` from `@expo/ui` | +| Collapsible section | `Collapsible` from `@expo/ui` | + +> **`List` is NOT a virtualized scrolling list.** It renders native grouped table rows — the visual look of an iOS Settings screen or a form section, with disclosure indicators and native row styling. Each `ListItem` is a native node on the JS thread; rows are not recycled. For any list with large or unknown-length data (feeds, search results, catalogs), use **`FlatList`** or **`FlashList`** instead. `List` is the right choice for short, fixed-length groups: a settings screen, a detail panel's rows, a fixed menu. + +**`BottomSheet` example** (use this for map pin details, action sheets, detail panels — not Reanimated): + +```tsx +import { Host, BottomSheet, Column, Text } from '@expo/ui'; +import { useState } from 'react'; + +export default function MapScreen() { + const [isOpen, setIsOpen] = useState(false); + + return ( + + setIsOpen(true)} /> + + setIsOpen(false)} + snapPoints={['half', 'full']} + > + + Café name + Address + + + + + ); +} +``` + +`BottomSheet` uses `isPresented`/`onDismiss` — **not** `isOpened`, `isOpen`, `onIsOpenedChange`, or `onChange` (those are `@gorhom/bottom-sheet` props and will silently do nothing). `snapPoints` accepts `'half'`, `'full'`, `{ fraction: 0.5 }`, or `{ height: 400 }` and is optional (auto-sizes to content when omitted). + +## Choosing an approach Work down this list and stop at the first layer that meets the need: diff --git a/plugins/expo/skills/expo-ui/references/universal.md b/plugins/expo/skills/expo-ui/references/universal.md index fb6170a..e71931c 100644 --- a/plugins/expo/skills/expo-ui/references/universal.md +++ b/plugins/expo/skills/expo-ui/references/universal.md @@ -61,6 +61,33 @@ export default function MyInput() { Docs — https://docs.expo.dev/versions/latest/sdk/ui/universal/textinput/index.md +## BottomSheet + +`BottomSheet` is a universal slide-up sheet. Use `isOpened` / `onIsOpenedChange` to control open state — **not** `isPresented`, `onDismiss`, or `snapPoints` (those are `@gorhom/bottom-sheet` props; the universal `BottomSheet` does not accept them). + +```tsx +import { Host, BottomSheet, Column, Button, Text } from '@expo/ui'; +import { useState } from 'react'; + +export default function MapScreen() { + const [isOpen, setIsOpen] = useState(false); + + return ( + + + + + Sheet content + + + + + ); +} +``` + +> **Don't confuse the universal `BottomSheet` with the drop-in `@gorhom/bottom-sheet` replacement** (`@expo/ui/community/bottom-sheet`). They are different components with different APIs. Use the universal one when building new UI; use the drop-in only when migrating an existing `@gorhom/bottom-sheet` integration. See `./drop-in-replacements.md`. + ## Confirming the API `@expo/ui` is versioned with the Expo SDK (e.g. `56.0.x` for SDK 56) and its API can change between SDK versions, so the **installed package's TypeScript types (`.d.ts`) are the most reliable source of truth** — they match the version in your project, while the docs track latest. Read the relevant component's `.d.ts` from the installed `@expo/ui` package in `node_modules`. Use the docs as the human-readable reference: diff --git a/scripts/check-overview-routing.ts b/scripts/check-overview-routing.ts new file mode 100644 index 0000000..5ccb18c --- /dev/null +++ b/scripts/check-overview-routing.ts @@ -0,0 +1,59 @@ +#!/usr/bin/env bun + +import { mkdirSync, readFileSync, readdirSync, writeFileSync, existsSync } from "node:fs"; +import { dirname, join } from "node:path"; + +const SKILLS_DIR = "plugins/expo/skills"; +const ROUTER_SKILL = "expo-overview"; +const ROUTER_PATH = join(SKILLS_DIR, ROUTER_SKILL, "SKILL.md"); + +function listSkillNames(): string[] { + return readdirSync(SKILLS_DIR, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .filter((name) => name !== ROUTER_SKILL) + .filter((name) => existsSync(join(SKILLS_DIR, name, "SKILL.md"))) + .sort(); +} + +function writeSummary(markdown: string) { + const summaryPath = process.env.ROUTING_CHECK_SUMMARY_PATH; + if (!summaryPath) { + return; + } + + mkdirSync(dirname(summaryPath), { recursive: true }); + writeFileSync(summaryPath, `${markdown}\n`); +} + +function complete(success: boolean, markdown: string): never { + writeSummary(markdown); + console.log(markdown); + process.exit(success ? 0 : 1); +} + +const skillNames = listSkillNames(); +const router = readFileSync(ROUTER_PATH, "utf8"); + +// A skill is "routed" when it appears as an inline-code token: `skill-name`. +const missing = skillNames.filter((name) => !router.includes(`\`${name}\``)); + +const markdown = [ + "## Expo overview routing check", + "", + missing.length === 0 + ? `Passed. All ${skillNames.length} skills are referenced in \`${ROUTER_PATH}\`.` + : "Failed. New skills must be added to the `expo-overview` Skill Map so the router can dispatch to them.", + ...(missing.length === 0 + ? [] + : [ + "", + "Skills missing from the router:", + "", + ...missing.map((name) => `- \`${name}\``), + "", + `Add each one as an inline-code entry (\`\`${"skill-name"}\`\`) under a category in \`${ROUTER_PATH}\`.`, + ]), +].join("\n"); + +complete(missing.length === 0, markdown);