diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..5fb946b
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,25 @@
+# Changelog
+
+## 0.1.0-beta.0
+
+First public beta. The API may still change before 0.1.0.
+
+### Added
+
+- `DataGrid` component: fixed-height rows virtualized with FlashList, sticky header, and columns pinned to the left.
+- Sorting: tap a header to cycle ascending, descending, unsorted. Stable, natural string order, empty values last. Custom `compare` per column.
+- Selection: `single` and `multiple`, with a pinned checkbox column, select-all, and controlled or uncontrolled state.
+- Search and filters: `searchText` that ignores case and accents, plus `text`, `values`, `range` and `custom` column filters. The search index is built on the first search and reused until `data` or `columns` change.
+- Server-driven data: `manualSorting`, `manualFiltering`, `loading`, `loadingMore`, pull-to-refresh, `onEndReached`, and `error` with retry.
+- Custom cells through `renderCell`, display text through `format`.
+- Light and dark themes, with every color overridable, following the device by default.
+- Row-level accessibility labels in visual column order, with a selection action.
+- Exported helpers: `sortRows`, `nextSort`, `filterRows`, `buildSearchIndex`, `matchesColumnFilter`, `getCellText`, `getCellValue`, `lightTheme`, `darkTheme`.
+
+### Known limitations
+
+- Every mounted row renders every column. Rendering only visible columns is planned for wide tables.
+- Sorting, filtering and key generation process the whole dataset in JS.
+- No column resizing or inline editing yet.
+- No native horizontal scrollbar or overscroll bounce: sideways scrolling is driven by a pan gesture so the header and pinned columns stay in sync ([#3](https://github.com/ahmdshrif/react-native-datagrid/issues/3)).
+- Performance has been measured on the iOS simulator and an Android emulator only. Physical low-end Android devices are not measured yet.
diff --git a/README.md b/README.md
index 98398a2..bdd71d8 100644
--- a/README.md
+++ b/README.md
@@ -1,32 +1,56 @@
-# react-native-datagrid
+
react-native-datagrid
-Fast data grid for React Native: pinned columns, sticky header, virtualized rows, sorting and selection.
+
+ A data grid for React Native with pinned columns that actually stay pinned.
+
-- **Pinned columns and sticky header** that follow sideways scrolling on the UI thread, so they never lag behind the body
-- **Virtualized rows** with [FlashList](https://shopify.github.io/flash-list/): only rows on screen are mounted. Sorting, filtering and key generation still process the whole dataset in JS
-- **Sorting**: tap a header to cycle ascending, descending, unsorted. Stable, natural string order, empty values last
-- **Server-driven data**: manual sorting and filtering, loading, pull-to-refresh, load more, and error states with retry
-- **Search and filters**: quick search that ignores case and accents, plus text, value, range and custom column filters
-- **Selection**: single or multiple, with a pinned checkbox column and select-all
-- **Custom cells** through `renderCell`, or plain text through `format`
-- **Light and dark themes**, with every color overridable
-- Pure JS on top of FlashList, Reanimated and Gesture Handler: works with Expo, no native code of its own
+
+ Quick start ·
+ API ·
+ Server data ·
+ Theming ·
+ Limitations
+
-> Status: early development (v0.1). The API may still change.
+
+
+
+
+
+
-## Installation
+
+
+
+
+> **Beta.** The API may still change before `0.1.0`. What lands next is decided by feedback from real apps — [open an issue](https://github.com/ahmdshrif/react-native-datagrid/issues).
+
+## Why this grid
+
+**⚡ Pinned columns that don't drift.** Sideways scrolling is driven by one Reanimated value, so rows, header and pinned columns move in the same frame. Feeding a `ScrollView`'s offset into a transform is a frame late on Android, and pinned columns visibly flicker ([#3](https://github.com/ahmdshrif/react-native-datagrid/issues/3)).
+
+**🚀 Built on FlashList.** Only the rows on screen are mounted, so 10,000 rows scroll like 30.
+
+**🎛 The parts business apps need.** Sorting, single and multiple selection with select-all, search and column filters, and a server-driven mode for sorting, filtering and paging on the backend.
+
+**🎨 Yours to style.** Light and dark themes that follow the device, every colour overridable, and `renderCell` for custom cells. Screen readers read each row as one sentence in column order.
+
+## Quick start
```sh
-npm install react-native-datagrid @shopify/flash-list react-native-reanimated react-native-worklets react-native-gesture-handler
+npx expo install react-native-datagrid@beta @shopify/flash-list react-native-reanimated react-native-worklets react-native-gesture-handler
```
-With Expo:
+
+Without Expo
```sh
-npx expo install react-native-datagrid @shopify/flash-list react-native-reanimated react-native-worklets react-native-gesture-handler
+npm install react-native-datagrid@beta @shopify/flash-list react-native-reanimated react-native-worklets react-native-gesture-handler
```
-Requires the New Architecture, FlashList 2, Reanimated 4 and Gesture Handler 2. If you don't use Expo, follow the [Reanimated setup guide](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started) to add the worklets Babel plugin.
+Follow the [Reanimated setup guide](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started) to add the worklets Babel plugin. On Expo, install the peers with `expo install` so they match your SDK; installing this library first can pull a Reanimated version that needs a newer React Native than your SDK ships.
+
+
Wrap your app in `GestureHandlerRootView` once, near the root:
@@ -38,9 +62,7 @@ export default function App() {
}
```
-Sideways scrolling uses a pan gesture rather than a `ScrollView`, so the header and pinned columns move in the same frame as the rows. A `ScrollView` reports its offset a frame late on Android, which makes pinned columns flicker.
-
-## Usage
+Then render a grid. It fills its parent, so give the parent a height.
```tsx
import { DataGrid } from 'react-native-datagrid';
@@ -75,147 +97,111 @@ export function Orders({ orders }: { orders: Order[] }) {
}
```
-The grid fills its parent (`flex: 1`), so give the parent a height.
+Memoize `columns`, or every row re-renders when the parent does.
## Columns
-| Option | Type | Description |
-| --- | --- | --- |
-| `key` | `string` | Unique id. Also reads `row[key]` when `getValue` is not set. |
-| `title` | `string` | Header text. |
-| `width` | `number` | Width in points. |
-| `pinned` | `'left'` | Keep the column visible while scrolling sideways. Pinned columns move to the front. |
-| `align` | `'left' \| 'center' \| 'right'` | Cell alignment. Default `left`. |
-| `sortable` | `boolean` | Let users sort by tapping the header. |
-| `getValue` | `(row) => unknown` | Read the value used for display and sorting. |
-| `compare` | `(a, b) => number` | Custom ascending comparator. |
-| `format` | `(value, row) => string` | Display text. Ignored when `renderCell` is set. |
-| `renderCell` | `({ row, rowIndex, value, column, selected }) => ReactNode` | Custom cell content. |
-| `searchable` | `boolean` | Include in `searchText` matching. Default `true`. |
-| `getSearchText` | `(row) => string` | Text used for search. Defaults to the shown text. |
-
-## Props
-
-| Prop | Type | Default | Description |
-| --- | --- | --- | --- |
-| `data` | `readonly T[]` | required | Rows. |
-| `columns` | `DataGridColumn[]` | required | Column definitions. Memoize them to avoid re-rendering rows. |
-| `keyExtractor` | `(row, index) => string` | required | Unique key per row. Use a stable ID from the row, not the index. |
-| `rowHeight` | `number` | `44` | Fixed row height. |
-| `headerHeight` | `number` | `40` | Header height. |
-| `striped` | `boolean` | `true` | Alternate row backgrounds. |
-| `sort` | `SortState \| null` | | Controlled sort. |
-| `defaultSort` | `SortState \| null` | `null` | Initial sort when uncontrolled. |
-| `onSortChange` | `(sort) => void` | | Called when a header is tapped. |
-| `searchText` | `string` | | Show rows whose searchable columns contain this text. |
-| `columnFilters` | `Record` | | Show rows matching every filter, by column key. |
-| `onFilteredCountChange` | `(count) => void` | | Number of rows left after filtering. |
-| `manualSorting` | `boolean` | `false` | Keep the order of `data`. The header still shows `sort` and taps still call `onSortChange`. |
-| `manualFiltering` | `boolean` | `false` | Show `data` without applying `searchText` or `columnFilters`. |
-| `loading` | `boolean` | `false` | First load. Shows a spinner only while there are no rows. |
-| `loadingText` | `string` | `'Loading'` | Text under the first-load spinner. |
-| `loadingMore` | `boolean` | `false` | Shows a spinner below the last row. |
-| `refreshing` | `boolean` | | Pull-to-refresh state. |
-| `onRefresh` | `() => void` | | Enables pull-to-refresh. |
-| `onEndReached` | `() => void` | | Called near the end of the list, to load the next page. |
-| `onEndReachedThreshold` | `number` | `0.5` | How far from the end `onEndReached` fires, in visible list lengths. |
-| `error` | `string \| null` | | Error message. Replaces the empty state when there are no rows, otherwise shows below the last row. |
-| `onRetry` | `() => void` | | Shows a Retry button next to `error`. |
-| `selectionMode` | `'none' \| 'single' \| 'multiple'` | `'none'` | `multiple` adds a pinned checkbox column. |
-| `selectedKeys` | `string[]` | | Controlled selection. |
-| `defaultSelectedKeys` | `string[]` | `[]` | Initial selection when uncontrolled. |
-| `onSelectionChange` | `(keys) => void` | | Called with all selected keys. |
-| `onRowPress` | `(row, index) => void` | | Row tap. In `multiple` mode, setting this makes row taps call it instead of toggling selection. |
-| `colorScheme` | `'auto' \| 'light' \| 'dark'` | `'auto'` | `auto` follows the device. |
-| `theme` | `Partial` | | Override theme tokens. |
-| `emptyText` | `string` | `'No rows'` | Shown when `data` is empty. |
-| `style` | `ViewStyle` | | Container style. |
-
-`sortRows`, `nextSort`, `filterRows`, `lightTheme` and `darkTheme` are exported too, for sorting on your own or building a custom theme.
-
-## Search and filters
+A column is data plus presentation: `width` and `pinned` place it, `format` or `renderCell` draw it, `sortable` and `compare` order it.
```tsx
-const [search, setSearch] = useState('');
-const [statuses, setStatuses] = useState([]);
-
-const columnFilters = useMemo>(
- () => ({
- status: statuses.length ? { type: 'values', values: statuses } : null,
- amount: { type: 'range', min: 1000 },
- }),
- [statuses]
-);
-
- row.id}
- searchText={search}
- columnFilters={columnFilters}
-/>;
+const columns: DataGridColumn[] = [
+ // Stays visible while scrolling sideways
+ { key: 'id', title: 'Order', width: 96, pinned: 'left', sortable: true },
+
+ // Custom cell content
+ {
+ key: 'status',
+ title: 'Status',
+ width: 120,
+ renderCell: ({ value }) => ,
+ },
+
+ // A value that isn't a plain field, with its own sort order
+ {
+ key: 'technician',
+ title: 'Technician',
+ width: 160,
+ getValue: (row) => row.assignee?.fullName ?? '',
+ compare: (a, b) => a.assignee.rank - b.assignee.rank,
+ sortable: true,
+ },
+];
```
-| Filter | Matches |
+Every option is in the [API reference](docs/api.md).
+
+## Features
+
+| | |
| --- | --- |
-| `{ type: 'text', value }` | Shown cell text contains `value`, ignoring case and accents. |
-| `{ type: 'values', values }` | Cell value equals one of `values` (dates compare by time). An empty list matches everything. |
-| `{ type: 'range', min?, max? }` | Number or date between `min` and `max`, inclusive. Cells that aren't valid numbers or dates never match; invalid bounds are ignored. |
-| `{ type: 'custom', test }` | `test(row)` returns true. |
+| **Sorting** | Tap a header to cycle ascending → descending → off. Stable, natural string order (`Item 2` before `Item 10`), empty values last. [Docs](docs/api.md#sorting) |
+| **Selection** | `single` or `multiple`, with a pinned checkbox column, select-all, and controlled or uncontrolled state. [Docs](docs/api.md) |
+| **Search & filters** | `searchText` ignores case and accents; per-column `text`, `values`, `range` and `custom` filters. [Docs](docs/api.md#filters) |
+| **Server data** | `manualSorting`, `manualFiltering`, loading, pull-to-refresh, load more, errors with retry. [Docs](docs/server-data.md) |
+| **Theming** | Light and dark, device-aware, every token overridable. [Docs](docs/theming.md) |
-The grid has no built-in filter controls yet, so you render your own search box and chips (see the example app). `filterRows`, `buildSearchIndex` and `matchesColumnFilter` are exported for filtering outside the grid.
+## Performance
-## Server-driven data
+Release builds, 10,000 rows × 13 columns, a checkbox column plus 2 pinned columns, over about 12 seconds of fast flings and sideways swipes. The numbers count frame intervals longer than 25 ms, a rough jank signal rather than a dropped-frame count:
-When a server sorts, filters or pages the data, turn off local processing and let the grid report what the user asked for. Your app owns fetching, pagination, retries and request cancellation; the grid only shows the states.
+| Platform | UI thread | JS thread |
+| --- | --- | --- |
+| iOS 26 simulator (Mac) | 0 long intervals | 0 |
+| Android emulator, API 35 (Mac) | 1 | 21 |
-```tsx
- row.id}
- sort={sort}
- onSortChange={setSort} // refetch page 1 with the new sort
- manualSorting
- manualFiltering
- loading={isFirstLoad} // spinner only while there are no rows
- loadingMore={isLoadingNextPage} // footer spinner, rows stay visible
- refreshing={isRefreshing}
- onRefresh={refetchFirstPage}
- onEndReached={loadNextPage}
- error={errorMessage}
- onRetry={retryLastRequest}
-/>
-```
+**Not measured on physical devices yet.** Emulator results move with host load, so they say nothing reliable about low-end Android phones. Device benchmarks are the gate before `0.1.0`.
-When a new sort or filter is loading and rows are already on screen, those rows stay visible until the new page arrives. The example app's **Server** tab shows a complete flow with a fake paged API, including a switch that makes the next request fail.
+What scales with the whole dataset, not just the visible rows: sorting, key generation, and the search index (built on the first search, then reused until `data` or `columns` change). Pass stable `data`, `columns` and `keyExtractor` references.
-## Performance
+Reproduce it yourself with `yarn example ios` or `yarn example android`, then switch on the FPS toggle in the example app.
-Early measurements on the example app (Release build, 10,000 rows × 13 columns, checkbox column plus 2 pinned columns, about 12 seconds of fast flings and sideways swipes). The numbers are frame callback intervals longer than 25 ms, a rough jank signal rather than a dropped-frame count:
+## Compatibility
-| Platform | UI thread long intervals | JS thread long intervals |
+| Package | Tested with | Peer range |
| --- | --- | --- |
-| iOS 26 simulator (Mac) | 0 | 0 |
-| Android emulator, API 35 (Mac) | 1 | 21 |
+| react-native | 0.83.10 | `*`, New Architecture required |
+| react | 19.2.0 | `*` |
+| expo | SDK 55 | not required |
+| @shopify/flash-list | 2.0.2 | `>=2.0.0` |
+| react-native-reanimated | 4.2.1 | `>=4.0.0` |
+| react-native-worklets | 0.7.4 | `>=0.5.0` |
+| react-native-gesture-handler | 2.30.0 | `>=2.20.0` |
+
+Checked on the iOS 26 simulator and an Android API 35 emulator in Release builds, plus a clean install into a fresh Expo app. Web is untested.
+
+## How it compares
+
+| | react-native-datagrid | Plain `FlatList` | [TanStack Table](https://tanstack.com/table) |
+| --- | --- | --- | --- |
+| Pinned columns + sticky header | Built in | You build it | You build it |
+| Row virtualization | FlashList | FlatList | Bring your own list |
+| Sorting and filtering logic | Built in | You build it | Built in, headless |
+| Server paging states | Built in | You build it | You build it |
+| Ready-made mobile UI | Yes | — | No, headless by design |
+
+There is also [`@bestcoder/react-native-data-table`](https://www.npmjs.com/package/@bestcoder/react-native-data-table), which covers FlashList virtualization, fixed left and right columns, sorting and selection — including right-pinned columns, which this library does not have yet. I have not benchmarked it; compare both before choosing.
-**Not yet measured on physical devices.** Emulator runs varied a lot with host load, so these results say nothing reliable about low-end Android phones. Device benchmarks with platform profiling are planned before a stable release. Background on the approach is in [docs/spike-results.md](docs/spike-results.md).
+## Limitations
-What scales with the full dataset: sorting (once per sort change), key generation, and the search index (built on the first search, reused until `data` or `columns` change). Pass stable `data`, `columns` and `keyExtractor` references to avoid repeating that work.
+- Fixed row height; rows do not auto-size.
+- Every mounted row renders every column. Rendering only visible columns is planned for wide tables.
+- No column resizing, reordering or inline editing yet.
+- Left-pinned columns only.
+- No native horizontal scrollbar or sideways overscroll bounce: that is the trade for keeping pinned columns in sync.
+- Not measured on physical low-end Android devices.
## Roadmap
-- Server-driven data: manual sorting and filtering, loading, refresh and load-more
-- Beta feedback decides what comes next (column resizing and inline editing are candidates)
-- Measure on real low-end Android phones and cut JS work per row
-- Later: rendering only visible columns for very wide tables, right-pinned columns, row grouping
+- Physical-device benchmarks, then `0.1.0`
+- Scroll reset when a new sort or filter is applied
+- Column virtualization for wide tables
+- Right-pinned columns
+- Column resizing and inline editing, if beta users ask for them
## Contributing
-- [Development workflow](CONTRIBUTING.md#development-workflow)
-- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
-- [Code of conduct](CODE_OF_CONDUCT.md)
+Issues and pull requests are welcome, especially reports from real apps with real tables. See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow and [RELEASING.md](RELEASING.md) for how releases are cut.
## License
-MIT
+MIT © [ahmdshrif](https://github.com/ahmdshrif)
diff --git a/RELEASING.md b/RELEASING.md
new file mode 100644
index 0000000..4cbd646
--- /dev/null
+++ b/RELEASING.md
@@ -0,0 +1,23 @@
+# Releasing
+
+## Beta
+
+1. `npm login` (once per machine).
+2. Make sure `main` is green in CI and your working tree is clean.
+3. Bump the version in `package.json` if needed, for example `0.1.0-beta.1`, and add a `CHANGELOG.md` entry.
+4. `yarn prepare` — builds `lib/` with bob.
+5. `npm publish` — `publishConfig.tag` is `beta`, so it will **not** become `latest`.
+6. `git tag v0.1.0-beta.0 && git push --tags`.
+
+Install instructions for testers: `npm install react-native-datagrid@beta`.
+
+## Stable
+
+Same steps, with `npm publish --tag latest`, after the beta has been confirmed on physical devices.
+
+## Checklist before publishing
+
+- `yarn lint`, `yarn typecheck`, `yarn test` pass.
+- `npm pack --dry-run` lists only `src/`, `lib/`, `README.md`, `LICENSE` and `package.json`.
+- The example app builds and runs in Release on both platforms.
+- A clean install into a fresh app renders the grid (see `docs/spike-results.md` for the method).
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 0000000..8f9a46c
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,104 @@
+# API
+
+## ``
+
+The grid fills its parent (`flex: 1`), so give the parent a height.
+
+| Prop | Type | Default | Description |
+| --- | --- | --- | --- |
+| `data` | `readonly T[]` | required | Rows. |
+| `columns` | `DataGridColumn[]` | required | Column definitions. Memoize them, or every row re-renders. |
+| `keyExtractor` | `(row, index) => string` | required | Unique key per row. Use a stable ID from the row, not the index. |
+| `rowHeight` | `number` | `44` | Fixed row height. |
+| `headerHeight` | `number` | `40` | Header height. |
+| `striped` | `boolean` | `true` | Alternate row backgrounds. |
+| `sort` | `SortState \| null` | | Controlled sort. |
+| `defaultSort` | `SortState \| null` | `null` | Initial sort when uncontrolled. |
+| `onSortChange` | `(sort) => void` | | Called when a header is tapped. |
+| `manualSorting` | `boolean` | `false` | Keep the order of `data`; the header still shows `sort` and taps still call `onSortChange`. |
+| `searchText` | `string` | | Show rows whose searchable columns contain this text. |
+| `columnFilters` | `ColumnFilters` | | Show rows matching every filter, by column key. |
+| `manualFiltering` | `boolean` | `false` | Show `data` without applying `searchText` or `columnFilters`. |
+| `onFilteredCountChange` | `(count) => void` | | Number of rows left after filtering. |
+| `selectionMode` | `'none' \| 'single' \| 'multiple'` | `'none'` | `multiple` adds a pinned checkbox column. |
+| `selectedKeys` | `readonly string[]` | | Controlled selection. |
+| `defaultSelectedKeys` | `readonly string[]` | `[]` | Initial selection when uncontrolled. |
+| `onSelectionChange` | `(keys: string[]) => void` | | Called with all selected keys. |
+| `onRowPress` | `(row, index) => void` | | Row tap. In `multiple` mode, setting this makes row taps call it instead of toggling selection. |
+| `loading` | `boolean` | `false` | First load. Shows a spinner only while there are no rows. |
+| `loadingText` | `string` | `'Loading'` | Text under the first-load spinner. |
+| `loadingMore` | `boolean` | `false` | Shows a spinner below the last row. |
+| `refreshing` | `boolean` | | Pull-to-refresh state. |
+| `onRefresh` | `() => void` | | Enables pull-to-refresh. |
+| `onEndReached` | `() => void` | | Called near the end of the list, to load the next page. |
+| `onEndReachedThreshold` | `number` | `0.5` | How far from the end `onEndReached` fires, in visible list lengths. |
+| `error` | `string \| null` | | Error message. Replaces the empty state when there are no rows, otherwise shows below the last row. |
+| `onRetry` | `() => void` | | Shows a Retry button next to `error`. |
+| `colorScheme` | `'auto' \| 'light' \| 'dark'` | `'auto'` | `auto` follows the device. |
+| `theme` | `Partial` | | Override theme tokens. See [theming](theming.md). |
+| `emptyText` | `string` | `'No rows'` | Shown when there are no rows to show. |
+| `style` | `StyleProp` | | Container style. |
+| `testID` | `string` | | Also sets `${testID}-list` and `${testID}-horizontal`. |
+
+## `DataGridColumn`
+
+| Option | Type | Description |
+| --- | --- | --- |
+| `key` | `string` | Unique id. Also reads `row[key]` when `getValue` is not set. |
+| `title` | `string` | Header text. |
+| `width` | `number` | Width in points. |
+| `pinned` | `'left'` | Keep the column visible while scrolling sideways. Pinned columns move to the front. |
+| `align` | `'left' \| 'center' \| 'right'` | Cell alignment. Default `left`. Right-aligned cells use tabular figures. |
+| `sortable` | `boolean` | Let users sort by tapping the header. |
+| `getValue` | `(row) => unknown` | Read the value used for display and sorting. |
+| `compare` | `(a, b) => number` | Custom ascending comparator. Reversed automatically for descending. |
+| `format` | `(value, row) => string` | Display text. Ignored when `renderCell` is set. |
+| `renderCell` | `(info) => ReactNode` | Custom cell content. `info` is `{ row, rowIndex, value, column, selected }`. |
+| `searchable` | `boolean` | Include in `searchText` matching. Default `true`. |
+| `getSearchText` | `(row) => string` | Text used for search. Defaults to the shown text. |
+
+## Sorting
+
+Tapping a sortable header cycles ascending → descending → unsorted.
+
+Sorting is stable, and empty values (`null`, `undefined`) always go last, in both directions. Numbers compare numerically, dates by time, booleans false-first, and strings with `Intl.Collator` using natural number order and no case or accent sensitivity, so `Item 2` comes before `Item 10`.
+
+```tsx
+const [sort, setSort] = useState({
+ columnKey: 'amount',
+ direction: 'desc',
+});
+
+;
+```
+
+## Filters
+
+| Filter | Matches |
+| --- | --- |
+| `{ type: 'text', value }` | Shown cell text contains `value`, ignoring case and accents. |
+| `{ type: 'values', values }` | Cell value equals one of `values` (dates compare by time). An empty list matches everything. |
+| `{ type: 'range', min?, max? }` | Number or date between `min` and `max`, inclusive. Cells that aren't valid numbers or dates never match; invalid bounds are ignored. |
+| `{ type: 'custom', test }` | `test(row)` returns true. |
+
+`searchText` matches against every column with `searchable !== false`, using what the cell shows (`format` output), so `$1,114.00` matches a search for `1,114`.
+
+The grid has no built-in filter controls; render your own search box and chips, as the example app does.
+
+## Exported helpers
+
+These are plain functions, usable without the component:
+
+| Function | Purpose |
+| --- | --- |
+| `sortRows(data, columns, sort)` | Returns a sorted copy, or `data` itself when nothing sorts. |
+| `nextSort(current, columnKey)` | The header-tap cycle: ascending → descending → `null`. |
+| `filterRows(data, columns, options)` | Applies `searchText` and `columnFilters`. |
+| `buildSearchIndex(data, columns)` | One searchable string per row; pass it back to `filterRows` to skip rebuilding. |
+| `matchesColumnFilter(row, column, filter)` | Tests one filter against one row. |
+| `getCellValue(row, column)` / `getCellText(row, column)` | The value and the shown text for a cell. |
+| `lightTheme` / `darkTheme` | The built-in theme token sets. |
+
+## Types
+
+`DataGridProps`, `DataGridColumn`, `CellRenderInfo`, `CellAlign`, `SortState`, `SortDirection`, `SelectionMode`, `ColumnFilter`, `ColumnFilters`, `DataGridTheme`.
diff --git a/docs/media/demo.gif b/docs/media/demo.gif
new file mode 100644
index 0000000..d8fdc22
Binary files /dev/null and b/docs/media/demo.gif differ
diff --git a/docs/server-data.md b/docs/server-data.md
new file mode 100644
index 0000000..8e40fd5
--- /dev/null
+++ b/docs/server-data.md
@@ -0,0 +1,66 @@
+# Server-driven data
+
+When a server sorts, filters or pages the data, turn off local processing and let the grid report what the user asked for. Your app owns fetching, pagination, retries and cancellation; the grid owns the states it shows.
+
+```tsx
+ row.id}
+ sort={sort}
+ onSortChange={setSort} // refetch page 1 with the new sort
+ manualSorting
+ manualFiltering
+ loading={isFirstLoad} // spinner only while there are no rows
+ loadingMore={isLoadingNextPage} // footer spinner, rows stay visible
+ refreshing={isRefreshing}
+ onRefresh={refetchFirstPage}
+ onEndReached={loadNextPage}
+ error={errorMessage}
+ onRetry={retryLastRequest}
+/>
+```
+
+## What each flag means
+
+| Prop | Behaviour |
+| --- | --- |
+| `manualSorting` | The grid keeps the order of `data`. The header still shows the sort arrow and taps still call `onSortChange`, so you can refetch. |
+| `manualFiltering` | The grid shows `data` as it is. `searchText` and `columnFilters` are ignored locally; use them to build your request. |
+| `loading` | Shown **only when there are no rows**. When rows are already on screen, they stay visible while the next result loads. |
+| `loadingMore` | A spinner below the last row. |
+| `error` | Replaces the empty state when there are no rows; otherwise appears below the last row so the loaded rows stay usable. |
+
+## States to handle
+
+- **First load:** `loading` true, no rows. The grid shows a spinner and `loadingText`.
+- **Changing sort or filters with rows on screen:** keep the old rows, set `loading` true. The grid keeps showing them, so the screen doesn't flash empty.
+- **Loading a page:** `loadingMore` true. Do nothing else; the rows stay put.
+- **Failure:** set `error`, and `onRetry` to repeat the last request. Keep the rows you already have.
+- **Empty result:** clear `loading` and `error` and pass an empty array; the grid shows `emptyText`.
+
+## Cancelling in-flight requests
+
+Sorting and typing produce overlapping requests. Cancel the previous one so a slow response can't overwrite a newer one:
+
+```tsx
+const controller = useRef(null);
+
+const load = useCallback(async (page: number) => {
+ controller.current?.abort();
+ const current = new AbortController();
+ controller.current = current;
+ try {
+ const result = await fetchRows({ page, sort, search, signal: current.signal });
+ setRows((prev) => (page === 0 ? result.rows : [...prev, ...result.rows]));
+ } catch (e) {
+ if (!current.signal.aborted) setError(String(e));
+ }
+}, [sort, search]);
+```
+
+## Known rough edge
+
+After a new sort or filter, the list keeps its scroll position, so a deep scroll can call `onEndReached` immediately. A scroll-reset option is on the roadmap. Until then, you can remount the grid with a `key` that changes with the query.
+
+A complete working version of all of this, with a fake paged API and a switch that makes the next request fail, is in the example app's **Server** tab: [`example/src/ServerDemo.tsx`](../example/src/ServerDemo.tsx).
diff --git a/docs/theming.md b/docs/theming.md
new file mode 100644
index 0000000..fec8f7d
--- /dev/null
+++ b/docs/theming.md
@@ -0,0 +1,67 @@
+# Theming
+
+The grid ships light and dark themes and follows the device by default. Override any token, or force a scheme.
+
+```tsx
+
+```
+
+## Tokens
+
+| Token | Used for |
+| --- | --- |
+| `background` | Row and grid background. |
+| `rowAlternateBackground` | Every second row when `striped`. |
+| `rowSelectedBackground` | Selected rows. |
+| `headerBackground` | Header row, including its pinned part. |
+| `headerText` | Header labels. |
+| `text` | Cell text and the active sorted header. |
+| `mutedText` | Empty state, footer messages, unchecked checkbox border. |
+| `border` | Row separators and header cell dividers. |
+| `pinnedEdge` | The line marking the pinned column edge, and the header's bottom border. |
+| `accent` | Sort arrow, checkboxes, spinners, Retry button. |
+| `onAccent` | The checkmark inside a checked checkbox. |
+| `fontSize` | Cell text size. Default `14`. |
+| `headerFontSize` | Header label size. Default `11`. |
+| `cellPaddingHorizontal` | Left and right padding inside every cell. Default `10`. |
+
+## Building on the defaults
+
+`lightTheme` and `darkTheme` are exported, so you can extend one instead of listing every token:
+
+```tsx
+import { darkTheme } from 'react-native-datagrid';
+
+const brandDark = { ...darkTheme, accent: '#FFB300', onAccent: '#1A1A1A' };
+
+;
+```
+
+## Matching your app's scheme
+
+Pass `colorScheme` when your app has its own theme switch, so the grid follows your setting instead of the device:
+
+```tsx
+const { isDark } = useAppTheme();
+
+;
+```
+
+## Cell-level styling
+
+Tokens cover the frame: backgrounds, borders, header and default cell text. For anything richer — status pills, coloured amounts, avatars — use `renderCell` and style it yourself:
+
+```tsx
+{
+ key: 'status',
+ title: 'Status',
+ width: 120,
+ renderCell: ({ value }) => ,
+}
+```
+
+Memoize the `columns` array, including any closures over your theme, or every row re-renders when the parent does.
diff --git a/package.json b/package.json
index dccf717..f8a26bd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-native-datagrid",
- "version": "0.1.0",
+ "version": "0.1.0-beta.0",
"description": "Fast data grid for React Native: pinned columns, sticky header, virtualized rows, sorting and selection",
"main": "./lib/module/index.js",
"types": "./lib/typescript/src/index.d.ts",
@@ -65,7 +65,9 @@
},
"homepage": "https://github.com/ahmdshrif/react-native-datagrid#readme",
"publishConfig": {
- "registry": "https://registry.npmjs.org/"
+ "registry": "https://registry.npmjs.org/",
+ "tag": "beta",
+ "access": "public"
},
"devDependencies": {
"@commitlint/config-conventional": "^21.2.0",