# versecafe/solid-native

[View on GitCafe](https://git.cafe/versecafe/solid-native)

Repository: [versecafe/solid-native](https://git.cafe/versecafe/solid-native)

Visibility: public

iOS and Android bindings for SolidJS v2 using Fabric \+ Hermes similar to react native

Default branch: main

## Resources

- [Source](https://git.cafe/versecafe/solid-native/tree/refs%2Fheads%2Fmain?format=markdown)

- [Commit history](https://git.cafe/versecafe/solid-native/commits/refs%2Fheads%2Fmain?format=markdown)

- [Issues](https://git.cafe/versecafe/solid-native/issues?format=markdown)

- [Pull requests](https://git.cafe/versecafe/solid-native/pulls?format=markdown)

- [Stacks](https://git.cafe/versecafe/solid-native/stacks?format=markdown)

- [Branches](https://git.cafe/versecafe/solid-native/branches?format=markdown)

- [Tags](https://git.cafe/versecafe/solid-native/tags?format=markdown)

Snapshot commit: 8ec111b04c5c361b1bd7ff86ec086a87a275264f

## README

[README.md](https://git.cafe/versecafe/solid-native/blob/8ec111b04c5c361b1bd7ff86ec086a87a275264f/README.md?format=markdown)

---

# Solid → Fabric prototype

Solid **2.0.0-rc.8** driving React Native **0.87.1** Fabric directly. The iOS and Android hosts
use RN's Hermes/runtime/surface infrastructure, but the bundle contains **no
React renderer or React Native JavaScript**. React is installed only to satisfy
RN's peer dependency. This is an experiment, not a mobile framework yet.

```text
TSX → official Solid Oxc compiler → esbuild → embedded JS bundle
                                              ↓
Solid universal renderer → logical nodes → Fabric JSI create/clone/completeRoot
                                              ↓
                           Yoga + native text → UIKit / Android views
```

## Run

### Create a separate app

```sh
bun run create:project /absolute/path/to/my-tally --name Tally --app-id dev.example.tally --scheme example-tally
```

The destination must not exist and must be outside this workspace. This creates
a **vendored framework workspace**, not an installed or published SDK. It includes
the renderer, explicitly registered native packages, hosts and tooling, plus a tiny
persisted counter in `app/`; it excludes Studio, tests, dependencies, SDKs, Pods,
build output, credentials and receipts. Ordinary file copies have no dependency
back to this directory. `framework-provenance.json` records SHA-256 source and
generated content hashes. No package publishing or generic autolinking is involved.

Inside the new directory run `bun install --frozen-lockfile`, `bun run typecheck`,
and `bun run build`, then follow its README for native setup and managed dev.
Bun's pinned hoisted setup is required by the native node_modules paths. All
registered packages are deliberately included, even those the counter does not use.
The root package name stays unchanged to preserve the pinned workspace lockfile;
it does not control the installed identity. Component/bootstrap imports remain
local source imports; device-service imports use the public workspace packages.

Optional `solid.config.json` sets `entry`, `displayName`, `appId`, and `urlScheme`.
The default Studio values are `demo/index.ts`, `SolidFabric`,
`dev.solid.fabric.prototype`, and `solid-native`. The starter uses `app/index.ts`,
`Tally`, `dev.solid.tally`, and `solid-tally`. App IDs must be lowercase dotted
identifiers. Entry paths must stay within the project. Xcode target/module names
and Kotlin/JNI namespaces stay implementation names; installed identities change.
Use a separate identity and scheme to leave Studio and its data untouched.

Android can explicitly reuse an installed toolchain without copying framework code:
`SOLID_ANDROID_TOOLCHAIN=/absolute/path/to/toolchain bun run dev --android`.
The directory contains `sdk/`, `user/avd/`, and `gradle-9.4.1/bin/gradle`; the default
is the new project's `android/.toolchain`. Without an installed
toolchain, `android:setup` explicitly downloads tools and accepts licenses.

### Run Studio

Requires Bun **1.4.0**, macOS, Xcode with an iOS simulator runtime, CocoaPods, and a Node
version supported by RN 0.87 (tested with Node 26). CocoaPods' Ruby environment
must include `xcodeproj`; the setup script also handles Homebrew CocoaPods.

```sh
bun install --frozen-lockfile
bun run check
bun run ios:setup
bun run ios:build

# Boot an iPhone simulator in Xcode/Simulator, then:
xcrun simctl install booted ios/build/Build/Products/Debug-iphonesimulator/SolidFabric.app
xcrun simctl launch booted dev.solid.fabric.prototype
```

`bun run ios:test` runs the native XCTest on an **iPhone 17 Pro** simulator. For
another device, run the same xcodebuild command with a different `-destination`.
First run `node scripts/network-fixture.mjs` in another terminal: the platform
networking tests require its loopback server on port 8083. Stop it with Ctrl-C
when testing finishes. This fixture is separate from the port-8082 reload server.
For keyboard UI tests, open Simulator and disable **I/O → Keyboard → Connect
Hardware Keyboard** so the software keyboard can appear.
The test taps controls, checks the counter, checks physical row positions after
a keyed move, hides/shows rows, captures both states, and checks a fresh launch.
Xcode's result bundle contains its screenshots and recording.

There is no Metro server or state-preserving hot reload. Debug builds can opt in
to full runtime reload below. Otherwise, after a TSX change, run `ios:build` and
reinstall, or run `bun run build` and rebuild in the generated Xcode workspace.
The Xcode project is generated; edit `scripts/ios-project.rb`, not the project.
Rerun `ios:setup` after changing the generator. Simulator signing is disabled.

### Repository layout and tooling

- The app opens **Studio**, a local notebook with native Notes, Saved, and Settings tabs.
  Read, compose, edit, bookmark, and share notes; notes and the haptics preference persist
  on the device. iOS uses grouped surfaces and blue accents; Android uses purple tonal
  surfaces and system tabs. **Tools** on Notes, or **Settings → Developer tools**, opens
  the original diagnostics; **Done** returns to Studio. No account or cloud sync is used.
- `src/`: reusable Solid renderer, components, navigation, and native-service adapters.
  - `renderer/`: Fabric tree, universal runtime, prop processing, JSX types, and inspector.
  - `components/`: native primitives, press handling, text input, scrolling, lists, and modal.
  - `navigation/`: router, link codecs, retained stacks, and tabs.
  - `motion/`: native animation graphs and composition.
  - `platform/`: host bootstrap, native modules/events, Back, window, keyboard, and safe area.
  - `services/`: networking and local notifications.
- `packages/`: Bun workspace packages for device services, storage, auth, web, and linking, each owning
  its TypeScript entry and iOS/Android sources. `@solid-native/native` is their shared
  internal module/event bridge, also used by the renderer host.
- `demo/`: application bootstrap and example screens; `demo/index.ts` is the default bundle entry.
- `test/`: Node protocol/build tests and Vitest component tests.
- `test/types/`: compile-only public API contracts, checked by TypeScript.
- `native/`, `ios/`, `android/`: shared Hermes factory and platform hosts/tests.
- `scripts/`: bundle tooling, fixtures, and native build/test orchestration.

Import implementation modules directly; `src/components/index.tsx` retains the existing
component entry point. The compiler-facing `@solid-native/runtime` and TypeScript's
`@solid-native/jsx-runtime` aliases resolve into `src/renderer/`.
These folders group responsibilities, not enforce independent layers: renderer and
motion retain their existing lifecycle coupling, and keyboard/safe-area wrappers use
renderer APIs. Platform bindings do not import the component entry point.

Bun manages packages and runs scripts. Commit `bun.lock`; use
`bun install --frozen-lockfile` for reproducible installs. The hoisted linker preserves
the `node_modules/react-native` paths used by the native hosts. Existing dependency
versions remain pinned. Node still runs the build/protocol scripts, and Vitest runs
component tests: use **`bun run test`**, not Bun's separate `bun test` runner.

```sh
bun run format        # Oxfmt: format JS/TS, configuration, and documentation
bun run format:check  # check without writing
bun run lint          # Oxlint: correctness + targeted strict rules, no warnings allowed
bun run lint:fix      # apply safe lint fixes
bun run check         # formatting, lint, tsc, both test suites, and bundle build
```

Oxfmt uses single quotes, semicolons, and a 100-column width without reordering
imports. Oxlint adds targeted control-flow, promise, and TypeScript checks to its
correctness rules. Explicit `any`, CommonJS imports, `@ts-ignore`, and `@ts-nocheck`
are errors; `@ts-expect-error` needs a description. Unused suppression comments
are errors too. Runtime/demo, app, and starter-template imports of React, React Native JS, and Solid's DOM
renderer are forbidden, including subpaths and type-only imports.
Intentional router generic erasure has narrowly documented exceptions; type-error
fixtures allow unused declarations and expressions. Nullish `== null` checks and
native callback interop remain allowed. JavaScript scripts and tests use Node globals
and reject undefined names and switch fallthrough. TypeScript owns these checks for TS,
with `strict`, `noImplicitOverride`, `noImplicitReturns`, `noFallthroughCasesInSwitch`,
`isolatedModules`, `verbatimModuleSyntax`, and `noUncheckedSideEffectImports` enabled.
`skipLibCheck` is disabled in the framework and generated projects, so TypeScript
also validates declaration files, including the pinned dependencies' declarations.
`exactOptionalPropertyTypes` and `noUncheckedIndexedAccess` are deferred: the installed
TypeScript 5.9.3 reports 66 and 62 diagnostics respectively, requiring deliberate
API and indexing migrations rather than blanket assertions. Promise lint rules are
syntax-level, not type-aware floating-promise analysis; the type-aware companion is
not installed and needs a separate dependency/version review. TypeScript remains the
type checker.
Generated projects retain app-aware `lint`, `lint:fix`, and `check` commands; their
`check` runs formatting, lint, typechecking, and bundling, without the omitted framework tests.
Native sources, dependencies, generated
projects, and build artifacts are excluded from both tools. Editor integrations
can use the committed `.oxfmtrc.json` and `.oxlintrc.json` configurations.
`check` does not run simulator/emulator suites; those remain explicit native commands.

### JavaScript development and debugging

```sh
bun run doctor                 # read-only native toolchain checks
bun run dev --ios              # managed Debug simulator session
bun run dev --android          # managed Debug device/emulator session
bun run dev --ios --device '<simulator UDID>'
bun run dev --android --device emulator-5554
```

Managed development selects one target, prepares/builds the Debug host when native
inputs change, verifies the installed app's bytes, installs only when needed, and
launches with fresh credentials. Android reverse forwarding is automatic and
session-owned. An ambiguous target requires `--device`; toolchain installation and
license acceptance remain explicit setup steps. iOS supports simulators, not physical
devices or LAN connections. Android can launch the existing project AVD when no
device is connected; it does not install an SDK or create an AVD implicitly.

Successful JS edits trigger **full runtime reload**, not state-preserving refresh.
On compile failure, the last successful bundle remains published; this does not
imply that the app is still running. The terminal distinguishes compiled,
published, downloaded, loaded, and surface-started revisions. “Running” means the
bundle reached its execution marker and the initial Solid/Fabric commit returned;
it does not prove a compositor frame or continuous application health. Packaged
startup is labelled separately from downloaded revisions.

Runtime exceptions do not roll back to the last good bundle. Managed iOS sessions
can recover from a failed startup evaluation when a corrected revision arrives.
On Android, an uncaught JavaScript exception can terminate the app under React
Native's exception policy. The CLI does not automatically restart a crashed app.

In a managed session, enter **`r` + Enter** to deliberately restart the verified
installed app with a new launch identity. This does not reinstall or rebuild;
published revisions and installed app data remain available. JavaScript state is
reset, not preserved. Enter **`q` + Enter** to exit the session.

Debug hosts persist bounded JavaScript diagnostics so the terminal can recover
evidence after the app exits. Diagnostics are matched to the session, launch and
exact executed bundle; source mapping uses that bundle's retained map, never the
current mutable build output. Missing/evicted maps and unrecognized frames fall
back to raw diagnostics. Retention is bounded, not a complete crash history.
An observed **process exit** is distinct from **process state unavailable**;
unavailable inspection does not prove a crash. An exit without fatal JavaScript
evidence is not labelled a JavaScript crash. Arbitrary native failures may have
no JavaScript stack.

If a fatal **packaged bootstrap** repeats before corrected JavaScript can load,
fix the source, enter `q`, and rerun the same managed command with **`--rebuild`**:

```sh
bun run dev --android --device emulator-5554 --rebuild
# Or: bun run dev --ios --device '<simulator UDID>' --rebuild
```

This managed-only flag explicitly bypasses a matching native receipt, rebuilds
the packaged bootstrap from the current successful JavaScript build, and verifies
and installs the artifact without clearing app data. `r` remains restart-only;
it never silently rebuilds the app.

Native receipts and frozen build inputs live in ignored `.solid/`. Native sources,
package native code, dependency/codegen inputs, lockfiles, configuration, and toolchain
identity determine rebuilds; application TSX and generated bundles do not. Installed
apps are checked against the cached artifact, not their version or install timestamp.
Native edits require restarting `dev`: inputs are verified during startup and before
publishing JS, and incompatible updates are withheld. Do not use a development-host
receipt as evidence that a packaged native test contains the latest JS.

`q` + Enter or Ctrl-C closes the watcher, transport, log processes, and owned forwarding. Existing
simulators and app data are preserved. One managed session owns the project and port
8082 at a time. If a process is forcibly killed, inspect the PID in `.solid/dev.lock`
before removing a stale lock; never remove another live session's lock or forwarding.
Release continues to use packaged JS and ignores development credentials.

The lower-level workflows remain available:

```sh
bun run dev                    # watch TSX; external source maps; files only
bun run build --sourcemap      # production maps are opt-in
bun run test:components        # Vitest: direct native TSX, no DOM
bun run test:watch              # watch the Vitest component tests
bun run test:ui                 # Vitest's local test UI
bun run symbolicate dist/main.jsbundle.map 120 9
```

`bun run test` runs both the Node protocol/build suites and Vitest. Vitest uses the
same Oxc universal transform and Solid client conditions as the native bundle;
it does not substitute Solid's nonreactive server exports. Mock Fabric tests
cannot verify native layout, IME, accessibility services, or gesture takeover.

For short iterations, run the affected Node suite (for example,
`node --test test/ui-building-blocks.test.mjs`) and `bun run typecheck` first.
Group native cases into one runner launch; repeated simulator/test-runner startup
costs much more than the JavaScript suites. XCTest's `test-without-building` is
useful only when both the installed bundle and test binary already match the
source being tested. Rebuild after TSX changes; a stale bundle is not a valid
shortcut. Keep native animation and asynchronous deadlines enabled.

Source maps compose Oxc's TSX mappings through esbuild. Symbolication takes
1-based stack line/column coordinates and resolves sources relative to the map.
Optional `--hermes /path/to/matching/hermesc` emits `.hbc` and `.hbc.map` siblings;
use the HBC map for Hermes bytecode `address at` frames, not the JS map. The
current native hosts still load JavaScript. Watch mode preserves the last good
output on compile failure; native reload requires explicit `--serve` opt-in.

### Opt-in full runtime reload (Debug only)

```sh
bun run dev --serve
# Copy the freshly printed bearer token into TOKEN in this terminal.
TOKEN='<printed token>'

# iOS Simulator: terminate an existing app before launching with the token.
xcrun simctl launch booted dev.solid.fabric.prototype --solid-dev-token "$TOKEN"

# Android emulator/device: force-stop an existing app before token startup.
android/.toolchain/sdk/platform-tools/adb reverse tcp:8082 tcp:8082
android/.toolchain/sdk/platform-tools/adb shell am start \
  -n dev.solid.fabric.prototype/.MainActivity --es solidDevToken "$TOKEN"
```

The server binds `127.0.0.1:8082`; clients use the fixed URL
`http://localhost:8082`. Tokens stay in launch configuration, not source files.
Restarting the server rotates its token and session: relaunch the app with the
new token. Release builds ignore this configuration and use packaged JavaScript.
Physical iOS devices and LAN endpoints are not supported.

Foreground native polling downloads immutable successful-build snapshots,
checks exact paths, byte limits and SHA-256, then publishes the cache and replaces
the runtime. **Every reload resets JS state; this is not HMR.** Compile errors,
failed downloads and invalid hashes retain the current application. A valid
build that throws at runtime can still break the new application: there is no
automatic runtime rollback. `--hermes` and `--serve` cannot be combined.

The previous bundle is retained for explicit recovery. On Android, send an
existing Activity `--es solidDevAction revert` (or `retry`) with the `am start`
command above, without the token extra. On iOS, the Debug AppDelegate exposes
`devReload.revert()` and `devReload.retry()` for native debugger use; no recovery
UI is provided. Revert stays pinned until retry. A reload timeout suspends
automatic reload to avoid overlapping runtimes; relaunch to recover. An
unexpected external iOS reload also suspends this client, so do not mix it with
Cmd-R or DevMenu reloads. Source maps are available on the server for
symbolication. Android downloads only the bundle; iOS currently verifies and
caches both artifacts, although only the bundle is needed to run JavaScript.

### Android

The setup script targets **macOS ARM64**, with Java 17+ already installed
(tested with Corretto 23). It downloads a checksum-verified Gradle 9.4.1 and
Android command-line tools, accepts local SDK licenses, and installs SDK 37,
build tools 37, NDK 27.1, CMake 3.22.1, and an API 36 ARM64 emulator under
`android/.toolchain`.
No Android Studio, Metro, RN Gradle plugin or separate Kotlin plugin is required;
AGP 9.2.1 provides Kotlin support.

```sh
bun run android:setup
bun run android:emulator # keep running in a separate terminal
bun run android:test     # builds, installs, tests; leaves the app installed

# Open the installed demo after tests close its Activity:
android/.toolchain/sdk/platform-tools/adb -s emulator-5554 shell am start \
  -n dev.solid.fabric.prototype/.MainActivity
```

`android:build` produces `android/app/build/outputs/apk/debug/app-debug.apk`.
`android:test` defaults to `emulator-5554` (override `ANDROID_SERIAL`) and writes
screenshots/logs under `artifacts/android`. It exercises native taps, physical
keyed order, hide/show, and Activity stop/relaunch using the Application's
existing Hermes host. A second test holds RN's private startup queue, destroys
the Activity while start is pending, and checks that the retired surface stops
and clears. It fails with stop-before-start ordering and passes when teardown
awaits startup off-main. The host includes `MainReactPackage` for native view
managers and pins Hermes to RN's exact Maven version, `250829098.0.17`.
A small appmodules library registers RN's core `FBReactNativeSpec_ModuleProvider`
so Java TurboModules are available without building RN native sources. Its ELF
load alignment is 16 KB. The current APK includes only `arm64-v8a`.
The local toolchain and build outputs require several GB, plus `~/.gradle` caches.
Stop the emulator with `android/.toolchain/sdk/platform-tools/adb -s emulator-5554 emu kill`.

Run `bash scripts/android-platform-test.sh` for navigation, motion, notifications,
networking and the new UI gallery. It requires the same port-8083 fixture above
and manages its own adb reverse mapping without stopping a server it does not own.
The optional `--fresh-permission` flag resets notification permission flags on
the disposable prototype app to exercise real dismissal and grant prompts.

## What this proves

- `<view>`, `<text>` and nested `<span>` map to `View`, `Paragraph`, `Text`, and
  `RawText` Fabric descriptors. JSX uses Solid's real universal compiler.
- Signal updates dirty the logical node and its ancestors. One queued microtask
  publishes a Fabric revision; clean branches retain their shadow references.
- Keyed moves retain node tags; removing a prop sends `null`, since omitted
  Fabric props otherwise retain their previous value.
- Logical sibling links avoid array searches/shifts during keyed reconciliation.
  Linking and sibling traversal are constant-time in sibling count; dirty marking
  still walks ancestors. `Node.children` is a lazy read-only ordered snapshot,
  rebuilt once when read after structural edits. Use renderer insertion/removal
  APIs rather than mutating it. Fabric commits still walk mounted membership and
  clone/append affected native child lists; native reordering is not constant-time.
- Native target events invoke Solid handlers through durable instance handles.
  Controls expose native accessibility button roles and activation callbacks.
- Surface stop disposes the Solid owner, commits an empty root, and prevents
  queued commits and detached-target events from reaching a disposed surface.
  Native teardown is attempted even when user cleanup throws. User cleanups
  must nevertheless be nonthrowing: Solid RC8 can abort remaining owner cleanup
  after an exception. Failures inside Universal's initial flush can also have
  their error replaced by a cleanup error; direct component setup errors and
  our initial Fabric commit errors are preserved.

The Node tests freeze published mock Fabric trees to detect accidental mutation,
and compile the actual demo using the same client package resolution as the app.
`bun run build` writes `dist/metafile.json` and rejects React/RN JS dependencies.
The Oxc + esbuild step is Babel/Metro-free; RN still brings its own native
codegen/build dependencies. Build timings printed by the script are local JS
build timings, **not** native-build or rendering benchmarks.

## Important native findings

**Hermes source block scoping must be enabled explicitly.** RN's stock Hermes
factory leaves this compiler option off; captured `for (let ...)` callbacks can
all observe the final loop index. The app-owned factory in `native/` preserves
RN 0.87.1 GC, microtasks, profiling and inspector behavior while enabling
`ES6BlockScoping`. Optional HBC builds pass the matching compiler flag. Native
runtime tests cover asynchronous loop captures. This does not enable separate
temporal-dead-zone checks or claim complete JavaScript specification conformance.

**Inline text uses a legacy wire name.** RN normalizes incoming `Text` to the
`Paragraph` descriptor and `VirtualText` to `Text`. The adapter therefore sends
`VirtualText` for `<span>` and nested `<Text>`. Sending `Text` creates a nested
paragraph attachment instead of an attributed run, breaking font inheritance.

**Clone empty, then append.** RN 0.87's bulk child-clone path can leave Yoga clean
when identically styled siblings reorder. The logical order changes but their
old native positions remain. This adapter follows RN's own renderer path:
clone with empty children, then append in order. The native position assertion
is a regression test for this; the JS mock alone cannot detect it.

**Native identity has limits.** Atomic same-parent keyed reorders preserve tags.
Reparenting materialized nodes and reinserting nodes after a committed removal
are rejected: Fabric families have immutable parents, and removal releases
their event targets. Showing conditional content creates fresh native nodes.
The retirement check walks the current tree per commit; this is correctness
bookkeeping, not an incremental-performance claim.

**Android colors and touch targets differ.** Numeric `color` / `*Color` props
are converted to signed 32-bit ARGB. Android's `Double.toInt()` otherwise clamps
unsigned values to half-transparent white. Demo controls use `box-only` hit
testing because Android Paragraph does not honor `pointerEvents="none"` like
a View. Pressable captures the closest willing responder and cancels on native
scroll takeover, cancellation, multiple fingers, disable, and removal.

**Minimal bootstrap is possible.** `environment.ts` obtains the real
`NativeMicrotasksCxx` binding and registers the host's log/device-event callback
destinations. `demo/index.ts` implements the small `RN$AppRegistry` / `RN$stopSurface`
contract invoked by native Fabric surfaces. It does not load RN `setup-env`.

**One pinned-version workaround.** The RN 0.87.1 generated eager-module list
includes `SampleTurboModule`, unavailable in the prebuilt iOS framework. The
app dependency provider filters that entry. No node_modules patches are used.

## Device packages

```ts
import * as haptics from '@solid-native/haptics';
import * as clipboard from '@solid-native/clipboard';
import * as sharing from '@solid-native/sharing';

await haptics.impact('light');
await haptics.notification('success');
await haptics.selection();

await clipboard.write('https://github.com/owner/repo');
const text = await clipboard.read(); // string | null

await sharing.share('Some text');
await sharing.share({ kind: 'url', content: 'https://github.com/owner/repo' });
```

Named imports work too. These are private local workspace packages, not published
npm releases. Run `bun install --frozen-lockfile`, then rebuild the native app;
the iOS generator and Android source sets include their native modules. JavaScript
package resolution uses normal exports, not custom aliases. Native implementations
use UIKit/Android APIs directly; ExpoModulesCore and React/RN JavaScript are not added.
Installing one of these packages into an unrelated host does not auto-link it.

- **Haptics:** impact styles are `light`, `medium`, `heavy`, `soft`, and `rigid`;
  notification kinds are `success`, `warning`, and `error`. The promise acknowledges
  the request, not completion of a physical vibration. Android approximates styles
  using system feedback and requires a foreground Activity. Device settings can
  suppress feedback. Simulator success does not prove physical output.
- **Clipboard:** `write(string)` replaces one text item, including empty text.
  `read()` requests first-item text without coercing URLs/images to strings. `null`
  means no text was exposed, not a reliable empty/denied distinction. Paste reads
  may trigger OS permission UI; invoke them from user actions.
  `write(text, { android: { sensitive: true } })` sets a preview hint on Android;
  it is not encryption or expiration and has no iOS effect.
  `getTypes()` returns advertised `text`/`html`/`image` metadata, not content.
  `onChange(({ types }) => ...)` returns an idempotent unsubscribe function and
  auto-cleans up inside a Solid owner. It does not emit an initial snapshot or
  guarantee background delivery; a subsequent read may see different content.
- **Sharing:** strings are always text. `{ kind: 'url', content }` explicitly shares
  an absolute HTTP(S) link. Empty content is rejected. Fulfillment means the native
  session ended, including cancellation—not that a recipient received anything.
  Concurrent calls reject `E_BUSY`; there is no queued sheet or timeout. Caller
  component disposal does not cancel the sheet. Host/module loss rejects the session.
  Optional `ios.anchor` uses `{ x, y, width, height }` in window-relative logical
  points, with a safe default for iPad. `android.chooserTitle` is presentation text,
  not an email subject. Pass these inside the second options argument.

Missing native modules fail explicitly. Clipboard HTML/image reads and writes,
file attachments, incoming shares, and Android-specific haptic escapes are not
exported yet. Rich content will extend `write`/`read`/`share` when its native
storage/permission lifecycle is implemented.

### Storage, authentication, and web

| Import                         | API                                                              |
| ------------------------------ | ---------------------------------------------------------------- |
| `@solid-native/storage`        | `open(namespace)` → `get`, `set`, `remove` for JSON preferences  |
| `@solid-native/storage/secure` | `get`, `set`, `remove` for Keychain/Keystore strings             |
| `@solid-native/auth/device`    | `capabilities`, `verify` for local device-owner checks           |
| `@solid-native/auth/passkeys`  | `create`, `get` for provider-owned WebAuthn credentials          |
| `@solid-native/web`            | `open`, `authenticate`, `createPkce` for system browser sessions |
| `@solid-native/linking`        | external `open` and app-root `onOpen` for incoming links         |

Browser sign-in belongs to `web`; device authentication does not substitute for
cryptographic secure-storage protection or server-side authentication. There is
no embedded WebView yet. Read the [storage contract](/versecafe/solid-native/blob/8ec111b04c5c361b1bd7ff86ec086a87a275264f/packages/storage/README.md?format=markdown),
[auth setup and limitations](/versecafe/solid-native/blob/8ec111b04c5c361b1bd7ff86ec086a87a275264f/packages/auth/README.md?format=markdown), and
[web/linking integration](/versecafe/solid-native/blob/8ec111b04c5c361b1bd7ff86ec086a87a275264f/packages/web/README.md?format=markdown) before building sign-in.
Passkeys need your signed app, associated domain, and a verifying server; the
sample does not configure a production relying party. Simulator Keychain access
uses simulator-only ad-hoc entitlements generated by `ios:setup`, not production signing.

Run `node --test test/capabilities.test.mjs` for package contracts. Native cases are
`SolidFabricNativeTests/NewPackageTests` on iOS and
`dev.solid.fabric.prototype.CapabilityModulesTest` on Android. These cover native
clipboard behavior and chooser lifecycle separately from the mocked JS contracts.
The new service contracts live in `test/{storage,auth,web-linking}.test.mjs`;
both native hosts include `StorageTests`, `AuthenticationTests`, and `WebLinkingTests`.
Android's browser test uses Chrome on the disposable emulator and completes its
first-run screen without an account. These tests do not establish passkey-provider
success or physical biometric protection.

## Component contracts and current limits

`src/components/index.tsx` exports typed `View`, `Text`, `Span`, `Image`, `ScrollView`,
`Switch`, `ActivityIndicator`, `Pressable`, `TextInput`, `Modal`, `VirtualizedList`,
`SafeAreaProvider`, `SafeAreaView`, and `KeyboardAvoidingView`. Nested `Text` becomes
an inline native text run. Styles support recursive arrays and conditional
entries; later styles override earlier ones and flat props. Colors support
hex3/4/6/8, integer `rgb`/`rgba`, transparent/black/white, and numeric ARGB—not
the complete CSS color grammar. Image sources use URI objects with explicit
layout dimensions; there is no asset registry or source-size inference.

Pressable render children and styles receive a stable reactive state object:
read `state.pressed` inside JSX or the style callback, rather than destructuring
it once during setup. Long press defaults to 500ms and suppresses ordinary press
when delivered. Missing native bounds suppress activation. Hover, ripple,
sound, delayed feedback, and multi-finger activation are not implemented.

TextInput uses raw native event payloads, not React synthetic events. Its ref
offers `focus`, `blur`, `clear`, and `setSelection`; selection offsets are UTF-16.
Accepted controlled edits do not issue a text-reset command. Rejected edits
restore with the latest native event count. Selection-before-change events can
run before Solid commits: reconciliation reads `latest()` to avoid momentarily
restoring stale text and cancelling composition. The hosted iOS native test
verifies marked Japanese composition and emoji selection through JS roundtrips.
Android direct InputConnection tests must isolate the installed keyboard;
they are not proof of full candidate-UI behavior with a real IME.

`src/navigation/router.ts` provides code-defined routes with typed `$params`, validated
search, asynchronous loaders, immutable history, and `push`/`replace`/`back`.
`router.bindBack(addBackHandler)` gives Android Back first refusal and preserves
native exit at the root. The diagnostics use `/`, `/components`, and `/platform`. Commands read
Solid's pending state with `latest`; rendering reads `router.state()` normally.
Loader cancellation uses a native-independent `NavigationSignal`, not a browser
`AbortSignal`. Superseded results cannot overwrite newer navigation. Configured
link prefixes restrict external URLs accepted by `openLink`; `buildLink` retains
typed route/search inputs. Versioned history export/restore validates serialized
input before replacing state. OS link delivery and persistent storage remain
application integrations, not automatic subscriptions.

`src/navigation/navigation.tsx` adds pathless layouts, retained `StackView` entries, focus
lifecycle, and lazy retained tabs with independent routers. Inactive screens
remain mounted but are hidden from layout, pointer input, and accessibility.
Use `useFocusEffect` for subscriptions that must stop on blur. Back pops the
active tab's stack before traversing tab history. These are Fabric view stacks,
not native navigation controllers: no interactive swipe-back, native headers,
file routing, shared-element transitions, or preload/cache policy.

`NativeStack` and `NativeTabs` provide native presentation over the same routers.
They use pinned `react-native-screens` 4.28.0 native components directly, without
its React wrappers or React Navigation JavaScript. iOS uses native navigation/tab
controllers; Android uses screens' native Fragment containers and system Back.

```tsx
const tabs = createTabs({
  initial: 'home',
  tabs: {
    home: { label: 'Home', create: createHomeRouter },
    activity: { label: 'Activity', create: createActivityRouter },
  },
});

<NativeTabs
  tabs={tabs}
  views={{
    home: {
      icon: { ios: 'house', android: 'ic_home' },
      render: (tab) => <NativeStack router={tab.router} views={homeViews} />,
    },
    activity: {
      icon: { ios: 'bell', android: 'ic_activity' },
      render: (tab) => <NativeStack router={tab.router} views={activityViews} />,
    },
  }}
/>;
```

Route view `options: screen => ({ title: screen.match().params.id })` computes
native headers reactively without remounting content. `header: false` hides the
header; `backGesture: false` disables iOS interactive Back. Icons name SF Symbols
on iOS and application-provided drawable resources on Android. The two platforms
keep their own appearance rather than sharing a custom-drawn tab bar.

Use one native presenter per router/tabs model. Presenters borrow those models;
unmounting a presenter releases its screens and Back handlers, not the router.
Native presenters register Back automatically: do not separately bind the same
model. Focus follows logical selection, inherits parent tab focus, and does not
change on a canceled back gesture. Native completed dismissal removes the exact
screen identity, preserving newer JS pushes and their loaders. Like ordinary
router Back, a completed native pop reloads the revealed route.

The initial native scope is 2–5 fixed tabs containing independent push stacks,
lazy first content mount, retained state, native titles/back buttons, iOS
interactive Back, and router push/replace/pop/restore. Native multi-pop menus and
repeated-tab pop/scroll effects are disabled. Navigation modals, custom header
JSX, large-title/search configuration, prevent-remove prompts, and arbitrary
nested native navigators are not part of this API. No predictive-back animation
parity is claimed. Native screen content handles its own header/tab insets; do
not add window safe-area padding around the navigator. Explore the working
fixture through **Tools → Native navigation** in the demo.

`animateOpacity(node, { from, to, duration })` in `src/motion/animation.ts` drives a
mounted `View` through native timing frames, not a per-frame JavaScript timer.
Start from an event or a guarded `onLayout`, not the initial ref callback.
It requires `collapsable={false}` and keeps the view non-flattened for its
lifetime. The returned `finished` promise reports completion/cancellation;
`cancel()` restores logical opacity rather than capturing an in-flight frame.
The default reduced-motion policy follows the OS; `always` and `never` are
explicit alternatives. Missing native capability rejects rather than silently
falling back to a JS tween.

`animateTransform` adds timing and physical springs for translateX/Y, rotation
in radians, and scaleX/Y. Use `transform2D({...})` for the fixed native transform
order; arbitrary transform arrays cannot be decomposed into this API. Opacity
remains timing-only. `sequence` and `parallel` accept lazy animation factories;
cancellation stops active children and prevents later sequence stages.

RN retains animated-property ownership after graph disconnection. This host
therefore retains separate opacity/transform graphs per ever-animated live node,
mirrors later logical writes through them, and drops them on node retirement.
Completion writes a final style override; a subsequent style/property write
reclaims logical control. A same-value Solid signal assignment is still a signal
no-op, not an imperative reset. There are no layout/shared-element transitions,
gesture-driven worklets, or Reanimated API.

`ScrollView` supports a fixed horizontal or vertical direction, native scrolling
commands through `scrollRef`, content-size events, paging and snapping props.
Remount the entire subtree to change direction. `VirtualizedList` windows known
row extents with stable keys; `renderItem(item, index)` receives accessors.
Offscreen row owners are disposed, so keep durable state in the data model.
Variable sizes require a deterministic `itemSize` function, not estimates.
Dynamic measurement, recycling, section lists and pull-to-refresh are not provided.

`Modal` presents a native host and requires `onRequestClose`. On iOS it retains
the host until native dismissal finishes; on Android hiding removes the Dialog
host. `SafeAreaProvider` waits for measured window insets unless given explicit
initial insets. `SafeAreaView` adds selected edges to numeric padding. Window
dimensions and keyboard hooks subscribe to core RN events; initial keyboard
visibility is unknown until an event arrives. `KeyboardAvoidingView` provides
padding-based avoidance, not keyboard-animation synchronization.

`src/services/networking.ts` exports `fetch`, `Headers`, `Request`, `Response`, and abort
classes over native RN Networking. Import them explicitly; installing globals
is opt-in. Requests and responses support buffered text/JSON, not streams,
binary data, multipart uploads, or browser cookie/redirect-control parity.
HTTP error statuses remain responses; transport failures reject. `timeoutMs`
sets a deadline and releases listeners. Android uses a JavaScript deadline plus
native abort to avoid ambiguous native timeout errors; suspended JavaScript can
delay that deadline. This is a deliberately limited fetch-shaped API, not a
complete browser Fetch implementation.

`src/services/notifications.ts` exposes local permission requests, scheduling/cancellation,
foreground receipt, response events, and consume-once initial responses. There
is no remote APNs/FCM registration or Expo push service. Android delays are
inexact and do not survive reboot; iOS permits at most 64 pending notifications.
Payload data must be a finite plain JSON object within 16KiB and 64 nesting
levels. Both platforms measure the same UTF-8 JSON serialized once by JavaScript,
not a platform-specific reserialization. Scheduling does not guarantee exact
delivery time.

`inspectSurface(surface)` from `src/renderer/inspector.ts` returns a detached JSON-safe
logical tree and last-submitted shadow-tree membership. It includes only
allowlisted scalar diagnostic props; text input values, text, URLs, selection,
labels, callbacks, and command arguments are omitted. Test IDs are opt-in via
`{ includeTestIDs: true }` because they can contain application identifiers.
This is explicit programmatic inspection—not actual native frames/visibility,
a mounting fence, source-linked inspection, or React DevTools integration.

- The wider Expo service catalog, asset pipeline, generic permission broker,
  remote push, portals/cross-surface moves, state-preserving HMR, and the full RN
  error-reporting environment are not integrated.
- `flush()` settles Solid, not native mounting/layout. Refs expose logical nodes,
  not UIKit views. Async/loading behavior needs dedicated native tests before
  claiming transition support. Multiple surfaces are not verified.
- No performance or leak claim yet. The VM tests exercise bootstrap restart in
  one JS runtime; Android tests retain the Application's host across Activity
  replacement. iOS tests still only cover process relaunch, not native surface
  restart in one runtime. Native stop-task completion is not a JS-cleanup
  barrier: RN schedules `RN$stopSurface` separately. Neither test establishes
  garbage-collection behavior.

Keep interaction tests on real Fabric, not just an increasingly elaborate mock.

## Renderer benchmarks and stress tests

Run `bun run benchmark` for a local Node/V8 architecture baseline. The harness
compiles real Solid client TSX through the universal compiler and uses an immutable
mock Fabric host. It measures mount/dispose, sparse updates, 100 coalesced writes,
and keyed reversals at 100, 1,000, and 10,000 mounted rows. Each workload has 10
warmups and 50 measured samples, reporting median/p95 and Fabric operation counts.
Correctness assertions run outside timing intervals; compilation is excluded.

It also runs 1,000 mount/dispose stress cycles after 100 warmups, checking row-owner
cleanup, listener removal, stale events, double disposal, and queued commits after
disposal. Post-GC V8 heap samples are diagnostic, not a leak-free guarantee. JSON
results with machine/runtime metadata are saved under `artifacts/benchmarks/`.
Run several fresh processes on an otherwise idle machine before comparing changes.

These timings include mock allocation/freezing and exclude native JSI, Yoga,
platform mounting, compositor frames, and Hermes. Rows are deliberately not
virtualized to expose scaling. They are not FPS, mobile startup measurements, or
evidence of a performance advantage over React Native. Timing thresholds are not
CI gates; failed correctness assertions do fail the command.

For real Hermes/Fabric measurements, boot an iOS simulator and run:

```sh
bun run benchmark:ios <simulator-UDID>
```

This requires the existing iOS setup. It builds Release with a separate installed
identity (`dev.solid.fabric.benchmark`), verifies the packaged bundle, runs a
dedicated fixture, and saves JSON under `artifacts/benchmarks/ios-*.json`. It does
not change Studio's bundle source or stored data. The runner stops its benchmark
app afterward and leaves the simulator booted. Do not run concurrent iOS builds
against the same `ios/build-release` directory.

The fixture uses 100/1,000/3,000/10,000 non-flattened native sibling views. It
measures single-row width changes and keyed reversals (5 warmups, 20 samples),
then checks 100 render/dispose cycles on the existing native root. Solid flush,
submission including flush, and native layout-event arrival are separate timings.
Only two rows subscribe to layout events; receipt timestamps exclude polling delay.
Layout events validate widths and positions but are **not UIKit mounting or
compositor fences**. Large reversals can saturate native mounting and take several
minutes. Simulator timings use the Mac CPU, not an iPhone CPU; no FPS, physical
device, native leak, or native-host restart claim follows from these results.

The native report also includes `virtualized` measurements for 1K/10K/100K
datasets: nonanimated jump submission, dataset-clone/layout-rebuild submission,
logical mounted-node counts, and row-owner cleanup. These loops await JS
microtasks, not native scroll completion, and use the initial 500-point viewport.
They establish bounded renderer work, not physical view counts, real gesture
scrolling, or frame smoothness.

Run `node scripts/benchmark-sparse.mjs` to compare one, ten, and one hundred
changed leaves across wide and nested trees of roughly 10K nodes in Node/mock
Fabric. The report includes clone/append counts, median/p95 submission timings,
and checks the submitted leaf values. It does not isolate phase durations or
measure nested-tree performance on Hermes.

## Pinned source contracts

- [RN Fabric JSI binding](https://github.com/facebook/react-native/blob/v0.87.1/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp)
- [Native surface startup/stop callbacks](https://github.com/facebook/react-native/blob/v0.87.1/packages/react-native/ReactCommon/react/renderer/uimanager/AppRegistryBinding.cpp)
- [Yoga child-clone layout behavior](https://github.com/facebook/react-native/blob/v0.87.1/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp)
- [Solid RC8 compiler options](https://github.com/solidjs/solid/blob/f8b40b7e2049d67ceebe1d2e90a1029eb64e097d/packages/compiler/types.d.ts)

