npm package: @haykal/ui-native
Description: React Native design system powered by **Uniwind** (Tailwind CSS for React Native). A publishable, extensible, monorepo-friendly component library.
Latest version: 1.0.0

# @haykal/ui-native

React Native design system powered by **Uniwind** (Tailwind CSS for React Native).
A publishable, extensible, monorepo-friendly component library.

## Installation

```bash
pnpm add @haykal/ui-native @expo/vector-icons expo-checkbox expo-image
```

## Quick Start

### 1. Configure Metro in your app

```javascript
// metro.config.js
const { withUniwindConfig } = require('uniwind/metro');
const config = getDefaultConfig(__dirname);

module.exports = withUniwindConfig(config, {
  cssEntryFile: './src/global.css',
  dtsFile: './src/uniwind-types.d.ts',
});
```

### 2. Create `global.css`

```css
@import 'tailwindcss';
@import 'uniwind';
@source '../../packages/ui-native/src';

@theme {
  /* Copy tokens from @haykal/ui-native/src/styles/global.css */
}
```

### 3. Import CSS first in entry file

```javascript
import './src/global.css'; // Must be first!
```

### 4. Use components

```tsx
import { Button, Checkbox, Icon, Switch } from '@haykal/ui-native';

<Button onPress={() => {}}>Save</Button>
<Switch
  label="Push Notifications"
  description="Receive important updates."
  value={enabled}
  onValueChange={setEnabled}
  variant="success"
  size="lg"
/>
<Checkbox
  label="Accept terms"
  value={accepted}
  onValueChange={setAccepted}
  size="sm"
/>
<Icon name="settings-outline" size={20} />
```

## Breaking Change (Switch + Checkbox)

- `checked` -> `value`
- `onCheckedChange` -> `onValueChange`
- Styled field API added: `label`, `description`, `variant`, and `size`

## Exports

| Path                             | Description                    |
| -------------------------------- | ------------------------------ |
| `@haykal/ui-native`              | Components + hooks + utils     |
| `@haykal/ui-native/styles`       | CSS theme tokens               |
| `@haykal/ui-native/utils`        | `cn()`, `cva()`, variant types |
| `@haykal/ui-native/hooks`        | `useColorScheme`, `useTheme`   |
| `@haykal/ui-native/primitives`   | `withUniwind` wrappers         |
| `@haykal/ui-native/action-sheet` | ActionSheet components & hooks |
| `@haykal/ui-native/types`        | Shared TypeScript types        |

## Key Components

- `Switch` (native `react-native` Switch with styled labels, variants, and sizes)
- `Checkbox` (native `expo-checkbox` with styled labels, variants, and sizes)
- `Icon` (Ionicons via `@expo/vector-icons`)
- `Button`, `Input`, `Textarea`, `Card`, `Avatar`, `Badge`, `Tabs`, `Accordion`, and more
- `ActionSheet` — iOS-like modal action sheet with imperative + declarative API

## ActionSheet

A provider-based action sheet built on [`@gorhom/bottom-sheet`](https://github.com/gorhom/react-native-bottom-sheet).

### Required peer dependencies

```bash
pnpm add @gorhom/bottom-sheet react-native-reanimated react-native-gesture-handler
```

> **Expo users:** install Reanimated via `expo-reanimated` and ensure Reanimated's Babel plugin is in your `babel.config.js`.

### Setup

Wrap your app root with `GestureHandlerRootView` (once) and `ActionSheetProvider` (once):

```tsx
// app/_layout.tsx  (Expo Router example)
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { ActionSheetProvider } from '@haykal/ui-native';

export default function RootLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <ActionSheetProvider>
        <Stack />
      </ActionSheetProvider>
    </GestureHandlerRootView>
  );
}
```

### Imperative API (recommended)

```tsx
import { useActionSheet, Button } from '@haykal/ui-native';

function PostActions() {
  const { showActionSheet } = useActionSheet();

  const onPress = async () => {
    const selected = await showActionSheet({
      title: 'Post actions',
      message: 'Choose an action below',
      actions: [
        { key: 'edit', label: 'Edit post' },
        { key: 'archive', label: 'Archive' },
        { key: 'delete', label: 'Delete post', variant: 'destructive' },
        { key: 'cancel', label: 'Cancel', variant: 'cancel' },
      ],
    });

    if (selected === 'delete') deletePost();
  };

  return <Button onPress={onPress}>Actions</Button>;
}
```

### Declarative API

```tsx
import { useState } from 'react';
import { ActionSheet, Button } from '@haykal/ui-native';

function Screen() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button onPress={() => setOpen(true)}>Open</Button>

      <ActionSheet
        open={open}
        onOpenChange={setOpen}
        title="More options"
        actions={[
          { key: 'share', label: 'Share' },
          { key: 'report', label: 'Report', variant: 'destructive' },
          { key: 'cancel', label: 'Cancel', variant: 'cancel' },
        ]}
        onSelect={(key) => console.log('Selected:', key)}
      />
    </>
  );
}
```

### Helpers

```ts
import {
  buildDestructiveConfirmOptions,
  buildShareOptions,
  createActions,
  menuToActionSheet,
} from '@haykal/ui-native';

// Typed action factory
const actions = createActions<'edit' | 'delete' | 'cancel'>([
  { key: 'edit', label: 'Edit' },
  { key: 'delete', label: 'Delete', variant: 'destructive' },
  { key: 'cancel', label: 'Cancel', variant: 'cancel' },
]);

// One-step destructive confirmation
const result = await showActionSheet(
  buildDestructiveConfirmOptions({ subject: 'post' }),
);
if (result === 'confirm') deletePost();

// Share-style sheet
const result = await showActionSheet(
  buildShareOptions({
    actions: [
      { key: 'copy', label: 'Copy link' },
      { key: 'twitter', label: 'Share to Twitter' },
    ],
  }),
);
```

### Troubleshooting

| Symptom                                                   | Fix                                                                            |
| --------------------------------------------------------- | ------------------------------------------------------------------------------ |
| "GestureHandlerRootView" not found                        | Add `react-native-gesture-handler` dep and wrap the root                       |
| Animations jank or don't run                              | Ensure Reanimated babel plugin is in `babel.config.js`                         |
| Sheet doesn't appear                                      | Make sure `ActionSheetProvider` wraps the component calling `useActionSheet()` |
| "useActionSheet must be inside ActionSheetProvider" error | Move `ActionSheetProvider` higher in the tree                                  |

## Documentation

See [IMPLEMENTATION_PLAN.md](docs/IMPLEMENTATION_PLAN.md) for architecture details.