Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions .github/workflows/change-scope.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,59 @@ name: Detect affected platforms
on:
workflow_call:
outputs:
library:
value: ${{ jobs.scope.outputs.library }}
ios:
value: ${{ jobs.scope.outputs.ios }}
android:
value: ${{ jobs.scope.outputs.android }}

permissions:
contents: read
actions: read

jobs:
scope:
runs-on: ubuntu-latest
timeout-minutes: 5
timeout-minutes: 140
outputs:
library: ${{ steps.detect.outputs.library }}
ios: ${{ steps.detect.outputs.ios }}
android: ${{ steps.detect.outputs.android }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fingerprint merged code and test inputs
id: inputs
env:
PR_BASE: ${{ github.event.pull_request.base.sha }}
PR_TITLE: ${{ github.event.pull_request.title }}
run: node scripts/ci-reuse.mjs prepare
- uses: actions/upload-artifact@v4
with:
name: ci-inputs-${{ steps.inputs.outputs.key }}
path: /tmp/keyflow-ci-inputs.txt
retention-days: 7
overwrite: true
- name: Reuse or await matching successful workflow
id: reuse
env:
GH_TOKEN: ${{ github.token }}
EVENT_NAME: ${{ github.event_name }}
PR_NUMBER: ${{ github.event.pull_request.number }}
INPUT_KEY: ${{ steps.inputs.outputs.key }}
run: node scripts/ci-reuse.mjs
- name: Detect changed platforms
id: detect
env:
REUSED: ${{ steps.reuse.outputs.reused }}
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: node scripts/ci-scope.mjs
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
run: |
if [ "$REUSED" = true ]; then
printf 'library=false\nios=false\nandroid=false\n' >> "$GITHUB_OUTPUT"
else
node scripts/ci-scope.mjs
fi
11 changes: 9 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,22 @@ on:
types: [opened, synchronize, reopened, edited]
workflow_dispatch:

# Keep matching earlier runs alive so documentation updates can await their result.
concurrency:
group: library-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.run_id }}
cancel-in-progress: false

permissions:
contents: read
actions: read

jobs:
scope:
uses: ./.github/workflows/change-scope.yml

check:
needs: scope
if: ${{ !cancelled() && (needs.scope.result != 'success' || needs.scope.outputs.library != 'false') }}
name: Format, types, tests, and package
runs-on: ubuntu-latest
timeout-minutes: 15
Expand Down
12 changes: 10 additions & 2 deletions .github/workflows/native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ on:
schedule:
- cron: '20 7 * * 1'

# Keep matching earlier runs alive so documentation updates can await their result.
concurrency:
group: native-${{ github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.run_id }}
cancel-in-progress: false

permissions:
contents: read
actions: read

jobs:
scope:
Expand Down Expand Up @@ -120,6 +122,9 @@ jobs:
matrix:
include: ${{ fromJSON(github.event_name == 'schedule' && '[{"label":"Android phone","profile":"pixel_6","api":36},{"label":"Android tablet","profile":"pixel_c","api":36},{"label":"Android older API","profile":"pixel_6","api":35}]' || '[{"label":"Android phone","profile":"pixel_6","api":36},{"label":"Android tablet","profile":"pixel_c","api":36}]') }}
steps:
- name: No android changes
if: ${{ needs.android.result == 'skipped' }}
run: echo "Android checks are unaffected or reuse verified successful results; see scope summary."
- name: Require successful shared build
if: ${{ needs.android.result != 'success' && needs.android.result != 'skipped' }}
run: exit 1
Expand Down Expand Up @@ -174,6 +179,9 @@ jobs:
matrix:
include: ${{ fromJSON(github.event_name == 'schedule' && '[{"label":"iPhone","device":"iPhone 17 Pro"},{"label":"iPad","device":"iPad Pro 11-inch (M5)"},{"label":"Large iPhone","device":"iPhone 17 Pro Max"},{"label":"Large iPad","device":"iPad Pro 13-inch (M5)"}]' || '[{"label":"iPhone","device":"iPhone 17 Pro"},{"label":"iPad","device":"iPad Pro 11-inch (M5)"}]') }}
steps:
- name: No ios changes
if: ${{ needs.ios.result == 'skipped' }}
run: echo "iOS checks are unaffected or reuse verified successful results; see scope summary."
- name: Require successful shared build
if: ${{ needs.ios.result != 'success' && needs.ios.result != 'skipped' }}
run: exit 1
Expand Down
26 changes: 19 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Real iPad example captures. These themes use the public API; Story Studio is exa

## Get started

The package is not published to npm yet. Run the example from this repository:
The first npm release is being prepared. Until it is published, install a [local package in your app](docs/api.md#compatibility-and-local-installation), or run the example from this repository:

```sh
git clone https://github.com/MeliValesca/react-native-keyflow.git
Expand All @@ -39,7 +39,7 @@ stim ios
# Or: stim android
```

The example uses **Expo SDK 57, React Native 0.86.3, and React 19.2.3**. Keyflow requires Expo Modules and a native development or production build; Expo Go and web are not supported. The example targets iOS 16.4+ and Android API 24+. See [compatibility and local installation](docs/api.md#compatibility-and-local-installation).
The example uses **Expo SDK 57, React Native 0.86.3, and React 19.2.3**. Keyflow requires Expo Modules and a native development or production build; Expo Go is not supported. On web, render your own fallback instead of `KeyflowTextInput`; see [web fallback](docs/api.md#web-fallback). The example targets iOS 16.4+ and Android API 24+. See [compatibility and local installation](docs/api.md#compatibility-and-local-installation).

### Your first input

Expand Down Expand Up @@ -218,16 +218,28 @@ Watch the MP4s: [iPad transitions](docs/media/ios-transitions.mp4) · [Android t
### Long press and accent selection

<table>
<tr><th>iPad · Hold and move between accents</th><th>Android phone · Hold for accents and shortcuts</th></tr>
<tr><th>iPad · Hold and slide between accents</th><th>Android phone · Hold and slide between accents</th></tr>
<tr>
<td><a href="docs/media/ios-accents.mp4"><img src="docs/media/ios-accents.gif" width="360" alt="iPad long-press accent grid with the theme’s rose selection highlight" /></a></td>
<td><a href="docs/media/android-accents.mp4"><img src="docs/media/android-accents.gif" width="360" alt="Android long-press popup showing accented letters and the number shortcut" /></a></td>
<td><a href="docs/media/ios-accents.mp4"><img src="docs/media/ios-accents.gif" width="420" alt="Slide between iPad accent choices with Story Studio’s rose selection highlight" /></a></td>
<td><a href="docs/media/android-accents.mp4"><img src="docs/media/android-accents.gif" width="420" alt="Android accent highlight following a continuous drag across rows and columns" /></a></td>
</tr>
</table>

Watch the MP4s: [iPad accent selection](docs/media/ios-accents.mp4) · [Android long press](docs/media/android-accents.mp4).

The previews are reduced to 10 fps; the MP4s retain the recordings’ timing. These are examples of Keyflow’s current behavior, not native-parity or physical-device performance benchmarks.
### Space-bar trackpad

<table>
<tr><th>iPad · Hold space, then move</th><th>Android phone · Slide on space</th></tr>
<tr>
<td><a href="docs/media/ios-trackpad.mp4"><img src="docs/media/ios-trackpad.gif" width="420" alt="iPad key labels fading during space-bar cursor movement and returning on release" /></a></td>
<td><a href="docs/media/android-trackpad.mp4"><img src="docs/media/android-trackpad.gif" width="420" alt="Android space retaining its pressed color while the cursor moves left and right" /></a></td>
</tr>
</table>

Watch the MP4s: [iPad trackpad](docs/media/ios-trackpad.mp4) · [Android trackpad](docs/media/android-trackpad.mp4).

The Android accent and trackpad GIFs use 50 fps; the iOS versions use 25 fps; the transition previews use 10 fps. The MP4s retain the recordings’ timing. These are examples of Keyflow’s current behavior, not native-parity or physical-device performance benchmarks.

The clips demonstrate the named interactions only. The other behaviors in the table are covered by the relevant [native and app test suites](docs/coverage.md), with device-review limits documented there.

Expand Down Expand Up @@ -301,7 +313,7 @@ Use `keyboardMode="system"` for the installed keyboard and whatever features its
### Integration limits

- `KeyflowTextInput` is single-line and native-owned. It has `defaultValue`, not a controlled `value`, and does not expose the complete React Native `TextInput` API—including secure-entry and semantic/AutoFill configuration props.
- Supported preview peers are Expo SDK 57, React Native 0.86.x (0.86.3+) and React 19.2.3+. Earlier combinations are not claimed as supported. A native build with Expo Modules is required; Expo Go and web are unsupported.
- Supported preview peers are Expo SDK 57, React Native 0.86.x (0.86.3+) and React 19.2.3+. Earlier combinations are not claimed as supported. A native build with Expo Modules is required; Expo Go is unsupported. On web, rendering `KeyflowTextInput` throws; provide your own [fallback](docs/api.md#web-fallback).
- Keyflow is an **in-app keyboard component**, not a system-wide keyboard extension/IME that users can install for other apps.

See [automated coverage and remaining manual checks](docs/coverage.md) for the precise boundary of the CI guarantees.
Expand Down
39 changes: 31 additions & 8 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,24 @@ npm install /tmp/react-native-keyflow.tgz

Use your app’s package manager if it differs, then rebuild its native app. To generate the standalone example repository instead, run `corepack yarn example:export` from Keyflow.

## Web fallback

The custom keyboard supports iOS and Android only. Importing the component is guarded against loading its native view on web, but rendering it on an unsupported platform throws. Choose your own fallback before rendering:

```tsx
import { Platform, TextInput } from 'react-native';
import { KeyflowTextInput } from 'react-native-keyflow';

export function CrossPlatformInput() {
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
return <TextInput placeholder="Start typing…" />;
}
return <KeyflowTextInput placeholder="Start typing…" />;
}
```

The fallback uses the browser’s normal input behavior; Keyflow’s keyboard theme does not apply to it.

## Input API

```tsx
Expand Down Expand Up @@ -81,6 +99,8 @@ await input.current?.setKeyboardMode('system');
await input.current?.focus();
```

These promises resolve after the native command is applied, not after the keyboard’s presentation or dismissal animation finishes. Use frame updates and the expected visible state when coordinating UI or writing tests.

Await `setKeyboardMode` before focusing. A mode change preserves text and selection, cancels active holds, and resets the custom keyboard page. If `keyboardMode` is controlled, keep its state synchronized through `onKeyboardModeChange`.

System mode delegates layout, languages, composition and settings to the user’s installed keyboard. Its visibility and floating/hardware-keyboard configuration remain controlled by the OS and IME. Keyflow’s colors and fonts cannot reskin that system keyboard.
Expand All @@ -95,6 +115,7 @@ Use `KeyflowAvoidingView` for the shared iOS/Android integration, with the activ
keyboardVerticalOffset={headerOffset}
enabled
style={{ flex: 1 }}
>
{/* Your content and KeyflowTextInput with onKeyboardFrameChange={setFrame} */}
</KeyflowAvoidingView>
```
Expand Down Expand Up @@ -137,14 +158,16 @@ The current native implementations are not fully aligned: Android’s active Cap
### Surface and material

```tsx
keyboardTheme={{
keyboard: {
background: '#16324F',
backgroundOpacity: 0.35,
keyOpacity: 0.7,
material: { type: 'raised', depth: 4, shadowColor: '#102030' },
},
}}
<KeyflowTextInput
keyboardTheme={{
keyboard: {
background: '#16324F',
backgroundOpacity: 0.35,
keyOpacity: 0.7,
material: { type: 'raised', depth: 4, shadowColor: '#102030' },
},
}}
/>
```

- `backgroundOpacity` (0–1) replaces the panel color’s alpha.
Expand Down
30 changes: 16 additions & 14 deletions docs/media/README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
# README media

The themed Keyflow captures were made on 2026-09-13 from the working tree at `c4183a6`. The four `default-*.jpg` previews reuse the earlier PR evidence described below. These demonstrate the current implementation, not a comparison against Apple/Gboard or a claim of physical-device frame pacing.

| Files | Device and action |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `ios-transitions.mp4` / `.gif` | Stim-owned iPad Pro 11-inch (M5), iPadOS 26.5. Story Studio composer: focus, dismiss, repeat, focus again |
| `ios-accents.mp4` / `.gif` | Same iPad. Hold E, then hold e and move to another accent |
| `android-transitions.mp4` / `.gif` | Stim Android phone emulator, Android API 36, 1080 × 2400. Story Studio composer: Back dismissal, refocus, repeat |
| `android-accents.mp4` / `.gif` | Same Android phone. Two stationary long presses on E/e expose accents and the number shortcut |
| `studio.jpg` | iPad Story Studio keyboard after accent input |
| `transparent.jpg` | iPad transparency example, default 35% panel and 70% keys, with typed text |
| `custom-font.jpg` | iPad custom-font example with bundled Quicksand SemiBold and typed text |
The themed stills and transition captures were made on 2026-09-13 from the working tree at `c4183a6`. Accent and trackpad clips were recorded again on 2026-09-14 using the keyboard implementation merged in `63227a8`. The four `default-*.jpg` previews reuse the earlier PR evidence described below. These demonstrate the current implementation, not a comparison against Apple/Gboard or a claim of physical-device frame pacing.

| Files | Device and action |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `ios-transitions.mp4` / `.gif` | Stim-owned iPad Pro 11-inch (M5), iPadOS 26.5. Story Studio composer: focus, dismiss, repeat, focus again |
| `ios-accents.mp4` / `.gif` | Same iPad. Two holds on e, sliding across columns and into the other accent row |
| `android-transitions.mp4` / `.gif` | Stim Android phone emulator, Android API 36, 1080 × 2400. Story Studio composer: Back dismissal, refocus, repeat |
| `android-accents.mp4` / `.gif` | Same Android phone. Hold E, then slide diagonally and horizontally across accent choices |
| `ios-trackpad.mp4` / `.gif` | Same iPad. Hold space twice and move the cursor left/right; key labels fade and return on release |
| `android-trackpad.mp4` / `.gif` | Same Android phone. One continuous space gesture moves the cursor left, right and left again while preserving pressed styling |
| `studio.jpg` | iPad Story Studio keyboard after accent input |
| `transparent.jpg` | iPad transparency example, default 35% panel and 70% keys, with typed text |
| `custom-font.jpg` | iPad custom-font example with bundled Quicksand SemiBold and typed text |

## Processing

- iOS recording: `xcrun simctl io <device> recordVideo --codec=h264 <file>` while XCTest drives the visible app.
- Android recording: `adb -s <device> shell screenrecord --bit-rate 4000000 <file>` while input events drive the visible app.
- Android recording: `adb -s <device> shell screenrecord` at 4–6 Mbps while input events drive the visible app. The new gesture recordings use one continuous touch stream sampled approximately every 16 ms; note text is entered through the visible custom keys.
- MP4s trim preparation/navigation, remove audio, and resize to 720 px wide with H.264, CRF 24 and fast-start metadata. Playback timing is unchanged.
- GIFs are 10 fps, 96-color previews. Transitions show the full screen at 280 px wide; accent previews crop to the bottom 45% at 360 px wide so the popup is legible.
- Android accent and trackpad GIFs are 50 fps; iOS versions remain 25 fps. Both use 128 colors at 480 px wide. The Android exports use the original recordings directly to preserve captured frames. The export frame rate does not imply that the source captured a new frame at every interval. They crop the bottom 45% on iPad and 50% on Android to include the input, popup and keyboard. Existing transition GIFs remain 10 fps, 96 colors and 280 px wide. Frames are sampled from the recordings without motion interpolation or playback speed changes.
- JPGs crop the bottom 40% of actual screenshots, resized to 900 px wide. No keys, colors, text, or backgrounds were reconstructed.

The original screenshot capture actions completed successfully. This capture harness is not part of the regression-test count. These small documentation assets are outside the npm package’s `files` allowlist.

To reproduce, launch the example with Stim, open Story Studio / Transparency / Custom app font, and perform the actions above. Keep media labels explicit about platform, form factor and capture processing.
To reproduce, launch the example with Stim, open Story Studio / Transparency / Custom app font, and perform the actions above. For trackpad captures, enter a short sentence through the custom keyboard, then move the cursor in both directions on space. On iOS, hold space before moving. Keep media labels explicit about platform, form factor and capture processing.

## Default-layout previews

Expand Down
Binary file modified docs/media/android-accents.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/android-accents.mp4
Binary file not shown.
Binary file added docs/media/android-trackpad.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/android-trackpad.mp4
Binary file not shown.
Binary file modified docs/media/ios-accents.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/ios-accents.mp4
Binary file not shown.
Binary file added docs/media/ios-trackpad.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/ios-trackpad.mp4
Binary file not shown.
Loading
Loading