This is the native half of [the monorepo blueprint](/guides/monorepo). It assumes an Expo application
created with `create-expo-app` and a shared kit that imports `asheeui`.

The renderer supplies components, not a platform. Expo still owns the application, the router, the
status bar and the build; AsheeUI supplies the design language and the components inside them.

## Versions this guide is written for

| Package | Version |
| --- | --- |
| Expo SDK | 58 |
| React Native | 0.88 |
| React | 19 |
| NativeWind | 4.2.7 (Tailwind 3.4) |
| `@asheeui/native` | 2.8.0 |

## 1. Install

```bash
npx expo install nativewind react-native-reanimated react-native-safe-area-context
npm install asheeui @asheeui/native
```

`react-native-reanimated` is what NativeWind's press and layout transitions ask for, and
`react-native-safe-area-context` is what the page shell measures its safe areas with. Install the React
Native copy the application already has rather than a second one.

**Do not install `react-native-css-interop` yourself.** NativeWind 4.2.7 brings its own copy, and that
copy is the half that turns a compiled class into a style. A pin of your own resolves *instead of* the
copy NativeWind brings: the Tailwind compile succeeds, the bundle contains every class name, and every
component renders unstyled with no warning anywhere.

The shared kit also needs the native TypeScript condition:

```jsonc
// tsconfig.json
{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "customConditions": ["react-native"],
    "types": ["node"]
  }
}
```

`customConditions` is what makes `asheeui` resolve to `@asheeui/native`'s types in the kit. `types:
["node"]` is only needed when the shared sources mention a Node global (they should not; it is cheaper
than a confusing error).

## 2. Babel

```js
// babel.config.js
module.exports = function (api) {
  api.cache(true);

  return {
    presets: [
      ["babel-preset-expo", { jsxImportSource: "nativewind" }],
      "nativewind/babel",
    ],
  };
};
```

Two presets, and the order matters. `babel-preset-expo` is the platform's own JSX transform, and
NativeWind's preset runs *after* it because the CSS transform works on the JSX output. NativeWind's
preset adds the Reanimated plugin, so the application does not list it again.

## 3. Metro

```js
// metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require("nativewind/metro");
const path = require("node:path");

const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, "../..");

const config = getDefaultConfig(projectRoot);
config.watchFolders = [workspaceRoot];
config.resolver.nodeModulesPaths = [
  path.resolve(projectRoot, "node_modules"),
  path.resolve(workspaceRoot, "node_modules"),
];
config.resolver.unstable_enablePackageExports = true;

module.exports = withNativeWind(config, { input: "./global.css" });
```

The workspace settings are what let Metro see the shared kit and read its `exports` map, which is how the
`react-native` condition applies. Do **not** set `disableHierarchicalLookup = true`: pnpm places a
package's own dependencies beside its real path in the virtual store, and hierarchical lookup is what
finds them.

## 4. The stylesheet

```css
/* global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
```

```tsx
// App.tsx
import "./global.css";
```

The file has to be both named in `metro.config.js` and imported. One that is named and never imported is
a bundle with no styles in it, which is the first thing to check when nothing is styled.

## 5. Tailwind: the umbrella's preset

```js
// tailwind.config.js
const asheeui = require("asheeui/tailwind-preset");

module.exports = {
  presets: [asheeui],
  content: [
    "./App.tsx",
    "./src/**/*.{ts,tsx}",
    "../../packages/kit/src/**/*.{ts,tsx}",
    ...asheeui.content,
  ],
};
```

The preset is three things: NativeWind's preset, the eight colour roles the components reference by
name, and the framework's own compiled modules in `content`. The spread is not decoration — Tailwind 3
replaces a preset's `content` with the application's own array, so the framework's glob has to reach the
application's list.

Do not state the roles as colours in `theme.extend.colors`. The preset states each role as the custom
property the provider paints; a hex there fixes the role at build time and stops it following
`defaultTheme` and `color`.

## 6. The root

```tsx
// App.tsx
import "./global.css";
import { StatusBar } from "expo-status-bar";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { AsheeUIProvider, ToastProvider } from "asheeui";
import { config } from "@acme/kit";
import { Shell } from "./src/shell";

export default function App() {
  return (
    <SafeAreaProvider>
      <AsheeUIProvider config={config}>
        <ToastProvider>
          <Shell />
          <StatusBar style="auto" />
        </ToastProvider>
      </AsheeUIProvider>
    </SafeAreaProvider>
  );
}
```

Order matters: the safe-area provider is outside because the page shell measures its insets; the
configuration provider is outside every component; the toast provider is above the screens that raise
toasts.

## 7. The shell: header and bottom bar

A phone's navigation is a header plus the destinations at the edge a thumb reaches. The shared screens
do not know about either.

```tsx
// src/shell.tsx
import { useState } from "react";
import { Pressable } from "react-native";
import { Navbar, Page, PageContent, TabBar } from "asheeui";
import {
  DashboardScreen,
  NutritionScreen,
  ProgressScreen,
  SettingsScreen,
  WorkoutDetailScreen,
  WorkoutsScreen,
} from "@acme/kit";

const links = [
  { label: "Today", href: "today" },
  { label: "Workouts", href: "workouts" },
  { label: "Nutrition", href: "nutrition" },
  { label: "Progress", href: "progress" },
];

/** The native replacement for an anchor: the application's own navigator. */
function TabLink({
  href,
  children,
  onNavigate,
}: {
  href: string;
  children?: React.ReactNode;
  onNavigate: (href: string) => void;
}) {
  return <Pressable onPress={() => onNavigate(String(href))}>{children}</Pressable>;
}

export function Shell() {
  const [route, setRoute] = useState("today");
  const [workoutId, setWorkoutId] = useState<string | null>(null);

  const navigate = (href: string) => {
    setWorkoutId(null);
    setRoute(href);
  };
  const link = { component: TabLink, props: { onNavigate: navigate } };

  return (
    <Page>
      <Navbar
        brand="Pulse"
        links={links}
        link={link}
        actions={<SettingsAction onPress={() => navigate("settings")} />}
      />
      <PageContent>
        {workoutId ? (
          <WorkoutDetailScreen id={workoutId} onBack={() => setWorkoutId(null)} />
        ) : route === "workouts" ? (
          <WorkoutsScreen onOpenWorkout={setWorkoutId} onNavigate={navigate} />
        ) : route === "nutrition" ? (
          <NutritionScreen onNavigate={navigate} />
        ) : route === "progress" ? (
          <ProgressScreen onNavigate={navigate} />
        ) : route === "settings" ? (
          <SettingsScreen onNavigate={navigate} />
        ) : (
          <DashboardScreen
            onNavigate={navigate}
            onOpenWorkout={() => setWorkoutId("lower-body")}
          />
        )}
      </PageContent>
      <TabBar items={links} link={link} />
    </Page>
  );
}
```

Every screen in the example is imported from the shared kit. The only native file is the shell, and the
only difference from the web shell is the `TabBar` and the state-based router.

## 8. Check the bundle

```bash
npx expo export --platform ios --output-dir dist
```

The export is the fastest proof that the application bundles: it resolves every import, runs Babel and
NativeWind, and writes a Hermes bundle. A missing peer, a wrong Metro setting or a class compiled outside
the `content` list fails here rather than on a device.

```bash
npx expo start          # Metro, for a simulator or Expo Go
npx expo run:ios        # a development build
```

## When nothing is styled

The failure has one shape: the right words, the right structure, no design at all. Check these in order.

1. **The stylesheet is never imported.** `global.css` is named in `metro.config.js` and not imported by
   `App.tsx`.
2. **The framework is not in `content`.** `...asheeui.content` is missing, so the framework's classes
   compile to nothing while the application's own components are styled.
3. **`react-native-css-interop` is pinned away from NativeWind's copy.** The compile succeeds and nothing
   is applied.
4. **A role is stated in `theme.extend.colors`.** The runtime palette stops following the configuration.
5. **The published artifact emits through React's JSX runtime.** This was a defect before 2.5.0 and is
   fixed; upgrade `@asheeui/native`.

The provider also checks the pipeline and warns once with the cause it found. The same diagnosis is
available as data:

```ts
import { verifyNativeWind } from "@asheeui/native";

const report = verifyNativeWind();
if (!report.ok) {
  for (const check of report.checks.filter((check) => !check.ok)) {
    console.log(check.summary, check.remedy);
  }
}
```

## What is different from the web

The full ledger is on [React Native](/platforms/native). The practical list for an application:

| Concern | Web | Expo |
| --- | --- | --- |
| Navigation | `Navbar` at the top | `Navbar` + `TabBar` |
| Press | `onClick` or `onPress` | `onPress` |
| Link | `href` renders an anchor | `href` opens through `Linking` |
| Responsive | media queries | `useBreakpoint` and `Grid columns*` |
| Overlays | portal and `z-index` | platform modal and sheet |
| Keyboard | the browser's | the system keyboard |
| Testing | Vitest + Testing Library on the DOM | Jest + React Native Testing Library |
