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
2 changes: 1 addition & 1 deletion demo/src/app/docs/docs-content.generated.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/capacitor-native-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,6 @@ For standard controls, follow the setup guide for your markup; the Ionic and HTM

- [Supported Ionic controls](docs/native-ui.md#supported-markup) and fallback behavior.
- [Configuration](docs/configuration.md): defaults, selected controls, rail-only mode, and live changes.
- [Light and dark appearance](docs/appearance.md): inherit the Web theme, configure custom HTML, and understand shared rail appearance.
- [Vertical Bars](docs/vertical-bars.md): eligibility, appearance, and runtime API.
- [Migration](docs/migration.md): package names, imports, and native project updates.
45 changes: 45 additions & 0 deletions packages/capacitor-native-ui/docs/appearance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Light and dark appearance

Native UI reads the computed style of each projected source element and sends its light/dark appearance to the native renderer. Configure appearance in your Web CSS; there is no separate native dark-mode startup option.

## How appearance is resolved

The first applicable row wins:

| Priority | Computed source style | Result |
| -------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| 1 | Theme-provided `--ios27-color-scheme` is non-empty | `dark` selects dark; other values select light. The iOS 27 theme supplies this marker through its light and dark stylesheets. |
| 2 | `--ion-background-color-rgb` supplies three RGB channels | Infers dark or light from the background brightness. |
| 3 | CSS `color-scheme` contains only `dark` or only `light` | Uses that scheme. `light dark` does not select a single appearance. |
| 4 | None of the above | Uses the system's `prefers-color-scheme: dark` setting. |

Theme markers and Ionic background values take precedence over `color-scheme`. Changing only an arbitrary CSS background or text color does not select native light/dark appearance.

## Match your Web theme

| Web setup | What to configure |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| iOS 27 theme | Load the matching [System, Class or Always dark-mode pair](../../theme-ios27/docs/setup.md#choose-one-dark-mode-pair). The theme supplies the appearance marker. |
| Ionic with an existing theme | Keep `--ion-background-color-rgb` consistent with the active palette; native appearance uses it when no theme marker is present. |
| HTML without those theme variables | Set `color-scheme: light` or `color-scheme: dark` to match the active Web theme. |
| System-managed HTML appearance | Use system-responsive CSS; when no single scheme is selected, native appearance falls back to the system setting. |

For example, a plain HTML app with a manual dark-mode class can declare:

```css
:root {
color-scheme: light;
}

:root.app-dark {
color-scheme: dark;
}
```

Use the same class for your application's dark palette. This example assumes neither of the higher-priority theme variables is supplied. Class/style mutations and system appearance changes trigger re-evaluation while the runtime is active. For style changes without an observed DOM mutation, call `shell.markForCheck()` after applying them.

## Shared Vertical Bars appearance

Ordinary controls resolve appearance from their own source styles. Vertical Bars shares one native rail appearance: **if any projected control is dark, the whole rail uses dark appearance**. It does not render separate light and dark rail regions. Keep sources projected into the same rail on a consistent Web palette.

Native light/dark appearance selects the system material and control appearance; it does not reproduce arbitrary application CSS. Vertical button fill and tint remain governed by the separate [button appearance settings](vertical-bars.md#choose-button-appearance).
12 changes: 11 additions & 1 deletion packages/capacitor-native-ui/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Without Ionic, import the same API from `@rdlabo/capacitor-native-ui/core`. That

Web rails require the Ionic rail layout or an application-owned HTML `webHost`. Settings never create a device capability.

For HTML-derived light/dark behavior, see [Light and dark appearance](appearance.md). Appearance is configured in Web CSS, not through a startup option.

## All settings

`controls` chooses selection and grouping defaults. `verticalBars` configures the rail. `ionic` configures Ionic discovery and appearance. Omitted fields default to `true` in `controls`, including partial objects.
Expand Down Expand Up @@ -94,7 +96,15 @@ Local `data-horizontal-bars="true|false"` and `data-vertical-bars="true|false"`
| `ionic.verticalBars.popover` | `false` | Opt into Ionic popovers anchored to native rail buttons. Requires the Ionic entry and its optional peers. |
| `debug` | `false` | Log source elements and fallback reasons to the developer console. |

`webHost` is an optional, application-styled HTML fallback rail. It must be connected in the same document and outside `root`, with neither element containing the other. Use a limited `root`, such as your `<main>`, when supplying a sibling host; the default `document.body` cannot have such a host. Without a host, unavailable HTML rails leave their original controls visible. Ionic's existing rail layout uses its own fallback adapter.
### HTML fallback rail host

`webHost` is an optional, application-styled HTML fallback rail. Its requirements are:

- Connected to the same document as `root`.
- Outside `root`; neither element may contain the other.
- Styled and positioned by the application, including any reserved content spacing.

Use a limited `root`, such as your `<main>`, with a sibling host; the default `document.body` cannot have such a host. Without a host, unavailable HTML rails leave original controls visible. Ionic's existing rail layout uses its own fallback adapter.

HTML currently supports buttons and back buttons. Tab, segment and FAB readers remain Ionic adapters; enabling those settings does not infer equivalent structures from arbitrary HTML.

Expand Down
31 changes: 25 additions & 6 deletions packages/capacitor-native-ui/docs/framework-independent.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,25 @@ The `/core` entry requires `@capacitor/core` but no Ionic dependencies. If this
<aside id="rail" aria-label="Navigation"></aside>
```

- `data-native-ui="button"` opts into a standard action button.
- `data-native-ui="back-button"` opts into a standard back button. Its original click handler owns navigation; the plugin never calls `history.back()`.
- `data-native-ui-placement="rail"` requests Vertical Bars. Without it, the source rectangle is used. `element` explicitly preserves that rectangle.
- Sources must be `<button>`, explicitly marked `ion-button`, or custom elements exposing `role="button"` and click behavior. Unmarked HTML is never inferred to be a control.

Keep the accessible name, supported static SVG, disabled state and application handlers on the source. Names resolve from `aria-labelledby`, `aria-label`, then text. Native `disabled`, a disabled fieldset and `aria-disabled="true"` are respected. Hidden, removed, excluded or out-of-scope sources are retired and stale actions are rejected. Use `data-native-ui="disabled"` to exclude a subtree.
| Attribute | Value | Effect |
| -------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `data-native-ui` | `button` | Project a standard action button. |
| `data-native-ui` | `back-button` | Project a standard back button. The source click handler owns navigation; the plugin never calls `history.back()`. |
| `data-native-ui` | `disabled` | Exclude the element and its subtree from projection. |
| `data-native-ui-placement` | `element` (default) | Use the source element's rectangle. |
| `data-native-ui-placement` | `rail` | Request Vertical Bars; use the optional Web rail host when native rail presentation is unavailable. |

Apply `button` and `back-button` to `<button>`, explicitly marked `ion-button`, or elements exposing `role="button"` and click behavior. Unmarked HTML is never inferred to be a control. `data-native-ui-placement` chooses placement for a declared source; it does not opt an unmarked HTML element into projection.

Keep the control's data and behavior on its source:

| Source property | Projection behavior |
| -------------------- | ------------------------------------------------------------------------------------------ |
| Accessible name | Resolves from `aria-labelledby`, then `aria-label`, then text. |
| Artwork | Uses supported static SVG. |
| Disabled state | Respects native `disabled`, a disabled fieldset, and `aria-disabled="true"`. |
| Visibility and scope | Hidden, removed, excluded or out-of-scope sources are retired; stale actions are rejected. |
| Application handlers | Remain on the original element. |

Native and Web-proxy actions click the original element. An ordinary `<button type="submit">` therefore keeps its original form behavior. Use `type="button"` for actions that must not submit. The native bridge does not preserve browser user-activation privileges for APIs such as popup opening.

Expand All @@ -51,6 +64,10 @@ const shell = await enableNativeUI({

Call once after the source root exists. Keep the handle and call `await shell.destroy()` when that root is disposed. The plugin does not add page padding or safe-area rules; retain your application's CSS. For Angular, React, or Vue, use the [app-root lifecycle examples](setup.md#place-startup-in-your-frameworks-app-root) with the `/core` import. For animated navigation, use [suspension through completion and cancellation](setup.md#navigation-without-the-theme).

### Match the HTML theme

For a custom HTML theme, declare `color-scheme: light` or `color-scheme: dark` in the same CSS that selects your Web palette. Background colors alone do not select native appearance. See [Light and dark appearance](appearance.md) for a manual-toggle example, precedence when Ionic/theme variables are present, and shared rail behavior.

## 4. Build and run on iOS

Build your Web app, run `npx cap sync ios`, and launch the iOS project from Xcode. With the step 3 configuration, Add renders natively and invokes its original click handler. With the rail configuration below, Add retains native element placement and Back uses the native rail when available. In a browser, the same button remains an ordinary Web button. Unmarked HTML remains unchanged.
Expand Down Expand Up @@ -83,6 +100,8 @@ Explicit markup chooses placement: Back goes to the rail and Add keeps native el

Currently the HTML adapter supports buttons and back buttons. Arbitrary HTML tabs, segments, searchbars and FAB structures are not translated. Ionic adapters continue to support their existing controls. Custom element extensions work with the same scoped root; the existing `tab-accessory` extension placement still requires an Ionic tab-bar owner.

For device-driven spacing and placement, see [Use with Capacitor Foldable](vertical-bars.md#use-with-capacitor-foldable). Read its initial placement and subscribe to changes, applying the edge and inset to your own CSS; the theme package is not required.

## Example

The [plain HTML example](https://github.com/rdlabo-dev/ionic-theme-ios27/tree/main/examples/native-ui-html) demonstrates Add, routerless Home/Details/Back navigation, optional Web rail projection and cleanup. It uses no Ionic or theme CSS.
34 changes: 30 additions & 4 deletions packages/capacitor-native-ui/docs/native-ui-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,15 @@ source.querySelector('button')!.addEventListener('click', () => {

The shell observes DOM changes and sends the new props to the native content. State rendered into DOM attributes or text is observed automatically. For properties or external state read by the reader that do not mutate the DOM, call `shell.markForCheck()` after changing them.

The reader is synchronous and read-only. Return `null` when the source should stay on the Web. Props are a JSON object (finite numbers, strings, booleans, null, arrays and objects), limited to 64K characters. Action names map to the source itself or its descendants; missing, hidden and disabled targets cannot be activated. Up to 30 actions are supported per source.
### Reader contract

| Part | Requirement |
| -------------- | -------------------------------------------------------------------------------------------------------------- |
| Execution | Synchronous and read-only. |
| Web fallback | Return `null` when the source should stay on the Web. |
| `props` | JSON object containing finite numbers, strings, booleans, null, arrays and objects; limited to 64K characters. |
| Action targets | The source itself or its descendants. Missing, hidden and disabled targets cannot be activated. |
| Action count | Up to 30 per source. |

## 2a. Register a SwiftUI View on iOS

Expand Down Expand Up @@ -227,10 +235,19 @@ For source-specific fallback reasons, temporarily enable [`debug: true`](configu
| Omitted or `'element'` | Hosts native content in the source element's Web frame on iOS 26+ or Android API 24+. |
| `'tab-accessory'` | Attaches the View to a projected tab bar's system SwiftUI tab accessory on iOS 26.1+. |

For `tab-accessory`, place the source inside its owning `ion-tabs`. The shell resolves that tabs element's `ion-tab-bar`; applications do not pass native controller references. Currently only the **Vertical Bars SwiftUI tab host** supports this placement. Ordinary UIKit tab bars, missing native tabs, iOS 26.0, or multiple competing accessories keep the source on the Web. The shell does not approximate a system accessory with a floating overlay. Keep an equivalent Web layout (for example, a fixed footer); the source retains its layout space while hidden.

Extensions are independent of `controls` and `ionic.auto`, including with `enableVerticalControlArea()`. The application controls their presentation: return `null` from the reader when a source should stay on the Web, then call `markForCheck()` when that condition changes. Accessories still require their supported native tab host.

### Tab accessory requirements

| Requirement | Supported configuration |
| ------------------- | ------------------------------------------------------------------------------------------------- |
| Source location | Inside its owning `ion-tabs`; the shell resolves that element's `ion-tab-bar`. |
| Native host | Vertical Bars SwiftUI tab host on iOS 26.1+. |
| Accessory count | A single accessory for the host. |
| Web fallback layout | Keep an equivalent Web layout, such as a fixed footer; the source retains its space while hidden. |

Ordinary UIKit tab bars, missing native tabs, iOS 26.0 or competing accessories keep the source on the Web. Applications do not pass native controller references, and the shell does not approximate a system accessory with a floating overlay.

## Read source geometry and presentation state

The shell measures the source DOM element automatically. With the default `element` placement, it positions and sizes the SwiftUI host to match that rectangle. You do not need to send coordinates in `props` or position the native view yourself.
Expand Down Expand Up @@ -359,7 +376,16 @@ The shell owns discovery, geometry, state synchronization, native controller con

Element placement requires the source to fit fully inside the viewport and its ancestors' overflow clipping bounds, including Ionic's shadow scroll containers. Partially clipped sources stay on the Web and become eligible again when scrolled fully into view. Tab accessories use the system host's placement instead.

`try await actions.activate(name)` completes after the original Web element receives its click. It does **not** wait for application work launched by the click handler (network requests, navigation or modal presentation). Disabled, removed or stale actions reject; teardown, native task cancellation and bridge timeouts settle pending calls. The bridge acknowledgement is a Capacitor Promise method.
### Action completion

| Event | Result of `try await actions.activate(name)` |
| ------------------------------------------------------------------ | -------------------------------------------- |
| Original Web element receives its click | Completes. |
| Handler starts a network request, navigation or modal presentation | Does not wait for that application work. |
| Action is disabled, removed or stale | Rejects. |
| Teardown, native task cancellation or bridge timeout | Settles pending calls. |

The bridge acknowledgement is a Capacitor Promise method.

The [demo reader](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/demo/src/native-ui/counter.ts) and [demo SwiftUI View](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/demo/ios/App/App/NativeUICounter.swift) show both placements using app-local files. Open the Native UI demo page to compare native projection with its Web fallback.

Expand Down
Loading
Loading