# `@coreframe/flutter-js`

Shared JavaScript shell and Dart host runtime primitives for Coreframe projects hosted in a Flutter
WebView. The npm artifact includes the path-consumable `coreframe_flutter` package under `dart/`.
Generated Coreframe apps resolve the Dart package from a pinned GitHub revision at
`flutter/flutter-js/dart`; maintainer-local template testing uses `pubspec_overrides.yaml` to point at this
checkout instead. The npm copy remains available for integrations that consume bundled Dart
packages from `node_modules`.

- `@coreframe/flutter-js/composition` binds the app's generated bridge, route contracts, and native
  page and surface controllers through framework-internal modules exposed by
  `@coreframe/vite/shell`. Applications consume the selected composition through
  `@coreframe/app/shell` and supply only their session hint, client setup, and application-scope
  cleanup hooks.
- `@coreframe/flutter-js/devtools` provides the debug-only page identity handshake used by Flutter
  development surfaces. The Dart package owns an independent Vite connection for managed-development
  status, actions, Codex progress, and restart controls. Vite brokers the page-owned state-launcher
  registry and commands to that connection. The Dart package also exports the matching mobile
  bottom-drawer widgets and their two-finger activation gesture.
- `@coreframe/flutter-js/native-bridge` runs the identity, sequencing, acknowledgement, retry,
  resynchronization, lifecycle, and scope-clearing state machine around an application's generated
  native-route contract. It also owns the fixed protocol schema and TypeScript codec; generated
  code supplies only the protocol version, app-specific route schemas, and discriminated types.
- `@coreframe/flutter-js/native-files` opens the host system's read-only file picker and returns
  immutable metadata plus runtime-scoped loopback bearer URLs. It does not put source paths,
  provider identifiers, permissions, or file bytes on the JavaScript bridge.
- `@coreframe/flutter-js/react` provides the persistent-WebView DOM readiness host and addressable
  native-route anchor used by a Flutter application's React composition.
- `@coreframe/flutter-js/router` projects React Router through the active Coreframe shell so browser
  and Flutter compositions keep the same route semantics.
- `@coreframe/flutter-js/shell` composes persistent root-WebView navigation, lifecycle, session
  observation, route-controller, and external-URL capabilities around an application's generated
  route contract and cleanup policy. It delegates native address matching and ranking to its
  required React Router peer.

Flutter-hosted applications should render internal destinations as anchors and use
`shell.navigation.navigate` for imperative redirects, form outcomes, and other programmatic
navigation. The shell preserves ordinary link affordances while routing unmodified same-origin
activations through the persistent-WebView handshake.

Flutter DOM routes can keep serializable, entry-local interaction state in a mutable proxy. The
shell restores one stable proxy for each retained native navigation entry and mirrors mutations to
that entry's React Router `location.state` without navigating or remounting the route:

```tsx
import { useSnapshot } from "@coreframe/lib/state"
import { useLocationStateProxy } from "@coreframe/app/router"

export function MessageComposer() {
  const state = useLocationStateProxy("composer", () => ({ draft: "" }))
  const view = useSnapshot(state)

  return (
    <textarea
      value={view.draft}
      onChange={(event) => {
        state.draft = event.target.value
      }}
    />
  )
}
```

The factory runs only when the entry has no object state stored under that key. Multiple consumers
using the same key on the same entry receive the same proxy; different keys own independent slices
of `location.state`. Tuple keys such as `["items", item.id]` provide item-specific slices and compare
by their string and number contents. Values must be structured-cloneable; route components, DOM
nodes, functions, and live resources do not belong in location state. Popped entries and
application-scope resets dispose their proxies automatically.

The Flutter stack also owns top-level DOM viewport restoration. Each Flutter-DOM entry saves its
window position when the persistent WebView is hidden; a new push starts at the top or its URL
fragment, while a pop restores the saved position before the native handoff cover reveals the live
WebView. Applications may render the shared `ScrollRestoration` facade for cross-platform source
compatibility. It uses React Router's implementation in the browser composition and is a no-op in
Flutter, where the shell owns restoration. Viewport records are discarded with their navigation
entries and on application-scope rotation.

The WebView uses ordinary same-origin browser requests at the app's fixed localhost origin. The Dart
runtime streams Worker-owned authentication, API, and WebSocket routes and forwards the WebView's
host-only, HttpOnly cookies only on those routes. It never stores or injects an application
credential. Development frontend requests and packaged assets receive no Worker cookies.
Applications continue to own Better Auth configuration, Worker token verification, session hints,
and an app-owned `clearSession` callback that uses the normal sign-out path. The native-route bridge
never carries credentials, authorization headers, access-token leases, or generic invocation
payloads.

## Native files

The standard Flutter host installs native file access by default. Check host support, select files,
and explicitly release them when the consumer finishes:

```ts
import {
  getNativeFileCapabilities,
  pickNativeFiles,
  releaseNativeFiles,
} from "@coreframe/flutter-js/native-files"

if ((await getNativeFileCapabilities()).available) {
  const files = await pickNativeFiles({ accept: ["image/*", ".pdf"], multiple: true })
  try {
    await upload(files[0].url)
  } finally {
    await releaseNativeFiles(files)
  }
}
```

Picker cancellation returns an empty array. Failures reject with `NativeFileError`, whose stable
codes are `invalid_request`, `unsupported_platform`, `picker_busy`, `unreadable_selection`,
`unsupported_filter`, `selection_not_accepted`, `capacity_exceeded`, and `internal`. The `accept`
option is a picker hint. Coreframe checks the selected filename and reported media type against the
complete requested union before returning it. A mismatch produces `selection_not_accepted`;
`unsupported_filter` means the selection did not provide enough metadata to evaluate the request.
These checks do not validate file contents, so applications must still validate files before using
them. A URL is a bearer credential: do not persist, log, or disclose it. It supports `GET`, `HEAD`,
and one byte range only while the current root Web runtime owns the selection. Explicit release,
root-document replacement, and host disposal revoke it. Responses are non-cacheable attachments and
cannot be used for document or embedded navigation.

## Declarative native surfaces

Register a `defineNativeSurface` controller when a React-owned feature should present a Flutter
widget without creating a route. Flutter codegen recursively scans `src/web/**/*.native.tsx` and
classifies each module by its direct default `defineNativePage` or `defineNativeSurface` call. It
statically reads the contract without executing application code:

```tsx
// src/web/app/flutter-shell/bottom-bar.native.tsx
import { defineNativeSurface, object, string } from "@coreframe/lib/native"

export default defineNativeSurface({
  surfaceId: "bottom-bar",
  props: object({ selectedTab: string() }),
  model: object({ selectedTab: string() }),
  actions: {},
  useController: ({ props }) => ({ actions: {}, model: { selectedTab: props.selectedTab } }),
})
```

No surface manifest is required. Native pages use the same `.native.tsx` convention but remain
referenced from `src/web/routes.ts`, because route registration owns address and presentation
policy. Flutter codegen also emits checked Dart models for native page and surface contracts. Nested
objects become Dart classes, literal unions become enums, and other unions become sealed variants.
It runs `json_serializable` before returning, so Flutter presenters import the generated `.dart`
model file and do not validate `NativeSurfaceEntry.model` or `NativeRouteModel.value` themselves.

After `pnpm coreframe flutter codegen`, mount the generated surface controller through the Flutter
shell facade:

```tsx
import { useState } from "react"
import { NativeSurface } from "@coreframe/app/shell"

export function NativeBottomBar() {
  const [active, setActive] = useState(true)

  return (
    <NativeSurface
      active={active}
      onDismiss={() => setActive(false)}
      presentation={{ backBehavior: "dismiss", statusBarForeground: "light", z: 1 }}
      props={{ selectedTab: "projects" }}
      surfaceId="bottom-bar"
    />
  )
}
```

`props` is an optional, typed input for the React-local controller. Declare its schema on
`defineNativeSurface`; codegen uses the generated controller type to require the corresponding
value at each `NativeSurface` call site. Coreframe validates the value before invoking
`useController`. These props stay in the Web/React composition and are not added to the native
bridge contract.

`NativeSurface` selects the controller from the generated registry and supplies its bridge identity
and host. It waits for native bootstrap, so SSR and the first pre-bootstrap render produce no
controller binding. Each mount receives a React-stable `instanceId`; pass an explicit `instanceId`
only when application state must coordinate that exact instance. Multiple mounts are independent.

Omitting `active` creates an uncontrolled surface that deactivates itself after native dismissal.
When `active` is supplied, `onDismiss` must update that value or unmount the component. The native
shell assigns `activationOrder`; application code supplies `backBehavior`, `routeAttachment`,
`transitionKey`, and `z` presentation policy. A surface may also set `statusBarForeground` to
`"light"`, `"dark"`, or `"automatic"`. The topmost presented surface with an explicit value
overrides the active route; omitting it leaves the route policy unchanged, including until a
dismissing surface finishes its native transition.

The facade is available only in the Flutter-selected composition. Browser and Tauri compositions
should render the feature's ordinary React implementation; they do not import the generated native
surface registry or Flutter controller modules.

Use `routeAttachment: "current"` for route-owned chrome that must share its route transition, such
as a native chat composer. The surface follows interactive gesture progress and the subsequent
commit or cancellation animation:

```tsx
<NativeSurface
  presentation={{ backBehavior: "ignore", routeAttachment: "current" }}
  surfaceId="chat-composer"
/>
```

Route attachment cannot be combined with an absolute `z` plane. Without it, activation and
dismissal continue to determine a surface's independent transition; use `z` for an absolute
compositor plane.
