@rdlabo/ionic-theme-ios27 1.2.0-1 → 1.2.0-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/css/components/ion-tabs.css +1 -1
- package/dist/css/ionic-theme-ios27.css +1 -1
- package/dist/css/vertical-bars.css +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -12
- package/dist/native/components/index.d.ts.map +1 -1
- package/dist/native/components/index.js +4 -1
- package/dist/native/components/ion-button.d.ts.map +1 -1
- package/dist/native/components/ion-button.js +3 -3
- package/dist/native/components/ion-buttons.d.ts.map +1 -1
- package/dist/native/components/ion-buttons.js +3 -2
- package/dist/native/definitions.d.ts +8 -15
- package/dist/native/definitions.d.ts.map +1 -1
- package/dist/native/definitions.js +1 -6
- package/dist/native/index.d.ts +1 -2
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +64 -28
- package/dist/native/prehide.d.ts.map +1 -1
- package/dist/native/prehide.js +6 -4
- package/dist/native/runtime.d.ts.map +1 -1
- package/dist/native/runtime.js +27 -12
- package/dist/native/shared/candidate.d.ts.map +1 -1
- package/dist/native/shared/candidate.js +8 -0
- package/dist/native/shared/dom.d.ts.map +1 -1
- package/dist/native/shared/dom.js +11 -10
- package/dist/native/shared/modal.d.ts +8 -0
- package/dist/native/shared/modal.d.ts.map +1 -0
- package/dist/native/shared/modal.js +143 -0
- package/dist/native/vertical-bars-web.d.ts.map +1 -1
- package/dist/native/vertical-bars-web.js +11 -3
- package/dist/sheets-of-glass/animations.d.ts +13 -4
- package/dist/sheets-of-glass/animations.d.ts.map +1 -1
- package/dist/sheets-of-glass/animations.js +56 -40
- package/dist/sheets-of-glass/index.d.ts +1 -1
- package/dist/sheets-of-glass/index.d.ts.map +1 -1
- package/dist/sheets-of-glass/index.js +64 -20
- package/dist/vertical-bars.d.ts +1 -2
- package/dist/vertical-bars.d.ts.map +1 -1
- package/dist/vertical-bars.js +0 -1
- package/docs/iphone-duo-with-original-theme.md +37 -15
- package/docs/iphone-duo.md +90 -47
- package/docs/migration.md +3 -3
- package/docs/native-ui-shell.md +7 -5
- package/docs/special-markup.md +0 -13
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +23 -4
- package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +15 -171
- package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +5 -0
- package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +30 -0
- package/package.json +1 -1
- package/src/index.ts +40 -9
- package/src/native/components/index.ts +6 -1
- package/src/native/components/ion-button.ts +4 -3
- package/src/native/components/ion-buttons.ts +4 -2
- package/src/native/definitions.ts +11 -19
- package/src/native/index.ts +67 -30
- package/src/native/prehide.ts +7 -4
- package/src/native/runtime.ts +28 -11
- package/src/native/shared/candidate.ts +10 -0
- package/src/native/shared/dom.ts +11 -10
- package/src/native/shared/modal.ts +157 -0
- package/src/native/vertical-bars-web.ts +12 -3
- package/src/sheets-of-glass/animations.ts +66 -41
- package/src/sheets-of-glass/index.ts +82 -16
- package/src/styles/components/ion-tabs.scss +7 -4
- package/src/styles/vertical-bars.scss +102 -35
- package/src/vertical-bars.ts +1 -2
package/docs/iphone-duo.md
CHANGED
|
@@ -10,13 +10,12 @@ Adapt your Ionic app to iPhone Duo: place navigation and actions in its vertical
|
|
|
10
10
|
|
|
11
11
|
Available in `1.2.0-0` as an **experimental** feature alongside [Native UI Shell](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell). APIs and supported behavior may change. The real system rail and hinge reporting require iOS 27.1 or later and an app built with Xcode 27.1 or newer.
|
|
12
12
|
|
|
13
|
-
This package provides
|
|
13
|
+
This package provides two independent pieces for that hardware. Each works **without the iOS 27 theme stylesheets** and **without the full Native UI Shell**:
|
|
14
14
|
|
|
15
15
|
- `dist/css/vertical-bars.css` — opt-in classes that reserve the rail's safe area, plus a registered custom property for a posture-driven split-pane width.
|
|
16
16
|
- `enableVerticalControlArea()` — moves eligible tabs and toolbar controls into the reserved area. On Capacitor iOS they are rendered by a native SwiftUI `TabView` and toolbar; everywhere else the same controls appear as Web clones.
|
|
17
|
-
- Device-layout reporting — the bundled Capacitor plugin reports rail placement, hinge status and the WebView corner radius through `getDeviceLayout()` and the `deviceLayoutChange` event.
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
Device state belongs to [`@erkamyaman/capacitor-foldable`](https://github.com/erkamyaman/capacitor-foldable). The **application** subscribes to its events and chooses its layout. This package's **stylesheet and runtime** apply that decision by reserving space, projecting controls and adapting the split pane. The theme does not monitor hinge state or bar placement.
|
|
20
19
|
|
|
21
20
|
## Choose what to adopt
|
|
22
21
|
|
|
@@ -24,10 +23,10 @@ To keep your existing theme and add only the standalone support, follow [iPhone
|
|
|
24
23
|
|
|
25
24
|
| Goal | Stylesheet | Runtime |
|
|
26
25
|
| ------------------------------------------------ | ------------------- | ------------------------------------------------------------------ |
|
|
27
|
-
| Hinge posture only (layout switches) | none | none — subscribe to
|
|
28
|
-
| Posture-driven split-pane width | `vertical-bars.css` | none — subscribe to
|
|
26
|
+
| Hinge posture only (layout switches) | none | none — subscribe to `Foldable` directly |
|
|
27
|
+
| Posture-driven split-pane width | `vertical-bars.css` | none — subscribe to `Foldable` directly |
|
|
29
28
|
| Vertical rail for tabs and toolbar actions | `vertical-bars.css` | `enableVerticalControlArea()` |
|
|
30
|
-
| Native shell plus the rail | `vertical-bars.css` | `enableNativeUIShell()` —
|
|
29
|
+
| Native shell plus the rail | `vertical-bars.css` | `enableNativeUIShell()` — includes rail projection |
|
|
31
30
|
|
|
32
31
|
```scss
|
|
33
32
|
@use '@rdlabo/ionic-theme-ios27/dist/css/vertical-bars.css';
|
|
@@ -39,39 +38,49 @@ The `/vertical-bars` entry point imports `@capacitor/core` at module load, so in
|
|
|
39
38
|
|
|
40
39
|
## Read the device layout
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
Install the device-state plugin in the application, then sync the native project:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install @erkamyaman/capacitor-foldable
|
|
45
|
+
npx cap sync
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use Capacitor 8.5 or later and build with Xcode 27.1 or newer for iPhone Duo's iOS 27.1 APIs. The dependency is needed for device-driven layout, not for the theme's CSS, browser simulation, or native control projection alone. Do not import the plugin's `ionic-tabs.css` alongside this package's rail projection; both would reposition the same tabs.
|
|
49
|
+
|
|
50
|
+
For a posture-driven split pane, subscribe directly without starting a projection runtime:
|
|
43
51
|
|
|
44
52
|
```ts
|
|
45
|
-
import {
|
|
46
|
-
import { HingeStatus, IonicNativeUIShell } from '@rdlabo/ionic-theme-ios27/vertical-bars';
|
|
53
|
+
import { Foldable, type FoldState } from '@erkamyaman/capacitor-foldable';
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
55
|
+
const applyFold = (fold: FoldState) => {
|
|
56
|
+
const pane = document.querySelector('ion-split-pane');
|
|
57
|
+
pane?.classList.toggle('ios-theme-split-pane-half-open', fold.state === 'half-opened');
|
|
58
|
+
const expanded = fold.state === 'half-opened' || (fold.state === 'flat' && !!fold.hingeBounds);
|
|
59
|
+
pane?.setAttribute('when', expanded ? '(min-width: 900px)' : '(min-width: 992px)');
|
|
60
|
+
};
|
|
61
|
+
let receivedEvent = false;
|
|
62
|
+
let disposed = false;
|
|
63
|
+
const listener = await Foldable.addListener('foldStateChange', (fold) => {
|
|
64
|
+
receivedEvent = true;
|
|
65
|
+
if (!disposed) applyFold(fold);
|
|
66
|
+
});
|
|
67
|
+
const initialFold = await Foldable.getFoldState();
|
|
68
|
+
if (!disposed && !receivedEvent) applyFold(initialFold);
|
|
69
|
+
|
|
70
|
+
// When the consumer goes away:
|
|
71
|
+
// disposed = true;
|
|
72
|
+
// await listener.remove();
|
|
60
73
|
```
|
|
61
74
|
|
|
62
|
-
`
|
|
75
|
+
`getFoldState()` and `foldStateChange` report `state` (`'flat'`, `'half-opened'`, or `'closed'`), `posture`, and optional hinge geometry. Without fold information, the plugin returns a flat state without `hingeBounds`; restore the ordinary split-pane breakpoint in that case. The Web implementation also returns a flat state. A half-opened state uses the 900px breakpoint even without hinge geometry; a flat state with hinge geometry also uses 900px. A closed state restores the ordinary 992px breakpoint. Events received during initialization take precedence over the initial read.
|
|
63
76
|
|
|
64
|
-
|
|
|
65
|
-
| ----------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
66
|
-
| `placement` | `{ edge: 'leading' \| 'trailing' \| null, inset }` — the rail's logical edge in the reading direction and its UIKit safe-area inset in points; `edge` is `null` on devices without a rail |
|
|
67
|
-
| `hingeStatus` | `HingeStatus.Closed`, `PartiallyOpen`, or `FullyOpen`; `null` when the device reports no hinge |
|
|
68
|
-
| `webViewMetrics.radius` | the WebView's effective top-left corner radius in points |
|
|
77
|
+
`getBarPlacement()` and `barPlacementChange` report `{ verticalBarEdge: 'leading' | 'trailing' | null, inset: number }`. The edge is **logical**: leading is the physical left in LTR and the physical right in RTL. Pass `{ edge: verticalBarEdge, nativeEdge: verticalBarEdge, inset }` to `setPlacement()`. No start/stop monitoring calls are needed; remove each listener when its owner is disposed.
|
|
69
78
|
|
|
70
|
-
|
|
79
|
+
The theme never reads UIKit bar-placement traits. The application supplies `nativeEdge` on the initial read and each event, even when choosing a fixed `edge`. Omit `nativeEdge` to keep the last supplied value; pass `null` when the plugin reports no edge. An explicit `nativeEdge: null` or an initial unregistered edge prevents native vertical projection: `enableVerticalControlArea()` keeps the requested Web rail, while the full Native UI Shell temporarily restores its ordinary horizontal layout. The requested rail is retained so a later reported edge can restore vertical layout. Native vertical projection starts only after a matching non-null edge is supplied. Omitting `nativeEdge` after supplying it preserves that value, including `null`. Passing `{ edge: null, nativeEdge }` updates the reported edge while keeping the rail disabled.
|
|
71
80
|
|
|
72
|
-
|
|
81
|
+
Foldable reports the measured width reserved by the native bar as `inset` and notifies changes through `barPlacementChange`. Pass this value to `setPlacement()` when following the reported edge, rather than assuming a fixed width. When no bar is reported, `inset` is `0`; the theme clears the explicit width and uses its CSS safe-area rules if the application still requests a Web rail. Applications choosing a different edge can supply their own width or use the CSS fallback. WebView corner radius remains a rendering concern: `configureNativeTransition()` uses the shell's `getWebViewMetrics()` API, independently of `Foldable`.
|
|
73
82
|
|
|
74
|
-
**
|
|
83
|
+
**Migration:** the theme's former `DeviceLayout`, `HingeStatus`, `getDeviceLayout()`, `deviceLayoutChange`, and start/stop device-layout monitoring APIs have been removed. Replace device subscriptions with the `Foldable` APIs above; use `getWebViewMetrics()` for one-shot radius reads. Foldable can infer Duo bar placement from safe-area insets when the app is built without the iOS 27.1 SDK. Hinge data still requires the newer SDK. Apps can also request a fixed rail placement independently of the reported edge.
|
|
75
84
|
|
|
76
85
|
## Reserve the vertical rail
|
|
77
86
|
|
|
@@ -81,13 +90,13 @@ Add `.ios-theme-vertical-bars` to `ion-app` to reserve the rail region on the ph
|
|
|
81
90
|
<ion-app class="ios-theme-vertical-bars">...</ion-app>
|
|
82
91
|
```
|
|
83
92
|
|
|
84
|
-
The classes are physical — `-left` always means the physical left edge — because CSS and the native renderer work in physical coordinates. `setPlacement` (below) is the usual way to apply them: it accepts the logical `
|
|
93
|
+
The classes are physical — `-left` always means the physical left edge — because CSS and the native renderer work in physical coordinates. `setPlacement` (below) is the usual way to apply them: it accepts the logical `verticalBarEdge` reported by `Foldable` and resolves it through the document's direction, so an RTL app does not need its own conversion.
|
|
85
94
|
|
|
86
|
-
For Chrome development, no native plugin is needed — the class alone reserves `80px` to simulate iPhone Duo. When `setPlacement` receives
|
|
95
|
+
For Chrome development, no native plugin is needed — the class alone reserves `80px` to simulate iPhone Duo. When `setPlacement` receives an explicit `{ edge, inset }`, that inset replaces the fallback width, even when it is less than `80px`. Override `--ios-theme-vertical-bars-safe-area-left` or `--ios-theme-vertical-bars-safe-area-right` when simulating a different layout.
|
|
87
96
|
|
|
88
97
|
This keeps routers and component backgrounds full-viewport. `ion-content` moves its scroll foreground, `ion-toolbar` moves its container foreground, and `ion-fab` adjusts only when placed beside the system UI. The corresponding Ionic safe-area variable is reset inside those foreground components so descendants do not add the inset again.
|
|
89
98
|
|
|
90
|
-
`ion-
|
|
99
|
+
`ion-modal` applies the same foreground correction when its visible dialog spans the viewport width. With the Vertical Control Area runtime enabled, the topmost full-width modal also projects eligible toolbar buttons into its own rail; centered dialogs keep their toolbar buttons and receive no page-rail inset. This includes full-width sheet modals: their rail follows the visible sheet bounds as the breakpoint changes. Eligibility follows the visible dialog width, not the hinge posture or modal type. `ion-menu` and `ion-popover` are handled as separate surfaces: their internal foreground components do not receive the main-page conversion and retain Ionic's standard safe-area handling. A menu presented beside the system UI keeps Ionic's full-viewport animation host and offsets only its visible container by the corresponding inset; a menu from the opposite side is unchanged. Left and right remain physical coordinates in RTL, while Ionic's `side="start"` and `side="end"` values remain logical.
|
|
91
100
|
|
|
92
101
|
The mode is component-mode independent: an app can keep Ionic `mode: 'md'` on iOS and still enable Vertical Bars. No component needs `mode="ios"`.
|
|
93
102
|
|
|
@@ -97,16 +106,19 @@ Start the standalone runtime once at application startup:
|
|
|
97
106
|
|
|
98
107
|
```ts
|
|
99
108
|
import { Capacitor, type PluginListenerHandle } from '@capacitor/core';
|
|
100
|
-
import { enableVerticalControlArea
|
|
109
|
+
import { enableVerticalControlArea } from '@rdlabo/ionic-theme-ios27/vertical-bars';
|
|
110
|
+
import { Foldable } from '@erkamyaman/capacitor-foldable';
|
|
101
111
|
|
|
102
112
|
// Start on Chrome too; the Web projection stays idle until the class is present.
|
|
103
113
|
const rail = await enableVerticalControlArea();
|
|
104
114
|
let layoutListener: PluginListenerHandle | undefined;
|
|
105
115
|
|
|
106
116
|
if (Capacitor.getPlatform() === 'ios') {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
layoutListener = await Foldable.addListener('barPlacementChange', ({ verticalBarEdge, inset }) =>
|
|
118
|
+
rail.setPlacement({ edge: verticalBarEdge, nativeEdge: verticalBarEdge, inset }),
|
|
119
|
+
);
|
|
120
|
+
const { verticalBarEdge, inset } = await Foldable.getBarPlacement();
|
|
121
|
+
rail.setPlacement({ edge: verticalBarEdge, nativeEdge: verticalBarEdge, inset });
|
|
110
122
|
}
|
|
111
123
|
|
|
112
124
|
// Call when the application owner is disposed.
|
|
@@ -118,15 +130,45 @@ const stopVerticalArea = async () => {
|
|
|
118
130
|
|
|
119
131
|
`setPlacement` on the handle and the exported `setVerticalControlAreaPlacement` are the same function; either applies the application's chosen placement to the CSS layout and both projections. It requires a mounted `ion-app` — call it after the app root exists.
|
|
120
132
|
|
|
121
|
-
- Pass
|
|
133
|
+
- Pass `{ edge, nativeEdge }`: `edge` is the application's chosen logical edge; `nativeEdge` is `verticalBarEdge` from `Foldable.getBarPlacement()`/`barPlacementChange`. They resolve through the nearest `dir` attribute, or the explicit `rtl` argument.
|
|
122
134
|
- Pass `null` to restore the ordinary layout.
|
|
123
|
-
- The device-layout listener reports what iOS chose; the application decides whether to apply it.
|
|
135
|
+
- The device-layout listener reports what iOS chose; the application decides whether to apply it. The theme compares the application's chosen edge with its supplied `nativeEdge`; a mismatch uses the Web rail until the edges match again. For a fixed right-in-LTR rail, pass `{ edge: 'trailing', nativeEdge: verticalBarEdge }` on each `Foldable` update.
|
|
136
|
+
|
|
137
|
+
Start either `enableVerticalControlArea()` or the full `enableNativeUIShell()` — not both. Repeating the same configuration returns the shared runtime; starting a different configuration while it is active throws an error. The application should have one owner responsible for destroying that runtime. If the app already uses `enableNativeUIShell()`, keep that single runtime and call `setVerticalControlAreaPlacement({ edge: verticalBarEdge, nativeEdge: verticalBarEdge, inset })` from its listener.
|
|
138
|
+
|
|
139
|
+
On supported iOS versions the runtime hands eligible tabs, back navigation, menu buttons, and fixed-toolbar actions to a native SwiftUI `TabView` and toolbar; on Web, Android, or when native projection is unavailable, Web clones remain the fallback. Back navigation can come from outside a fixed toolbar; menu buttons and other toolbar actions still require one.
|
|
140
|
+
|
|
141
|
+
### Toolbar actions
|
|
142
|
+
|
|
143
|
+
An `ion-button` moves into the rail when it contains an `ion-icon` or SVG with `slot="icon-only"`. The button must be in a fixed `ion-toolbar` directly inside `ion-header` or `ion-footer`, outside scrolling `ion-content`.
|
|
144
|
+
|
|
145
|
+
| Icon markup | Placement |
|
|
146
|
+
| --- | --- |
|
|
147
|
+
| `slot="icon-only"` | Vertical rail |
|
|
148
|
+
| `slot="start"`, `slot="end"`, or no slot | Original horizontal toolbar |
|
|
149
|
+
| No icon | Original horizontal toolbar |
|
|
150
|
+
|
|
151
|
+
This rule applies to `fill="default"`, `clear`, `solid`, and `outline`, including buttons with an Ionic `color`. Solid actions retain their background color through a prominent native button; outline actions retain their border color and width. The runtime preserves the original click or form-submit behavior. `type="submit"` and `.button-submit` do not select a different placement.
|
|
152
|
+
|
|
153
|
+
```html
|
|
154
|
+
<ion-header>
|
|
155
|
+
<ion-toolbar>
|
|
156
|
+
<ion-buttons slot="end">
|
|
157
|
+
<ion-button type="button" fill="outline" color="primary" aria-label="Done">
|
|
158
|
+
<ion-icon name="checkmark-outline" slot="icon-only"></ion-icon>
|
|
159
|
+
</ion-button>
|
|
160
|
+
</ion-buttons>
|
|
161
|
+
</ion-toolbar>
|
|
162
|
+
</ion-header>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Give icon-only actions an accessible name, such as `aria-label="Done"`. Keep the original Ionic event handler or form association on the source button.
|
|
124
166
|
|
|
125
|
-
|
|
167
|
+
The same rule applies to individual buttons and buttons inside `ion-buttons`, on ordinary pages and in the topmost full-width modal. Centered modals, menus, and popovers keep their own toolbar layout. Add `.ios-theme-horizontal-only` to a group or individual button to keep it horizontal. Placement is chosen when a routed page enters; changing an existing button's content or icon slot does not move it between the toolbar and rail until the page leaves and re-enters.
|
|
126
168
|
|
|
127
|
-
|
|
169
|
+
### Tab bar
|
|
128
170
|
|
|
129
|
-
When the app contains `ion-tabs`, its tab bar moves into the reserved region and
|
|
171
|
+
When the app contains `ion-tabs`, its tab bar moves into the reserved region and uses the native Duo edge spacing; the Ionic `slot` value does not select a different position. Without native projection, the stable Web rail is icon-only, matching the native resting presentation. While the user presses and drags across that rail, every icon-and-label tab reveals its label so the pending destination stays identifiable. The Web tab bar receives pointer input in the simulated system region. Native tabs and a restored Web tab bar fade in over 180ms; disappearance remains immediate. Reduced motion disables this fade. Use `ion-menu` when navigation should become a sidebar; this mode does not convert tabs into a menu. Web clones also work when no `ion-tabs` exists. Disabling the mode or leaving the page removes the native ownership or Web clones and restores their sources. Override `--ios-theme-vertical-bars-toolbar-top` when the simulated system controls use a different vertical layout.
|
|
130
172
|
|
|
131
173
|
## Adapt the split pane
|
|
132
174
|
|
|
@@ -154,7 +196,7 @@ ion-split-pane {
|
|
|
154
196
|
}
|
|
155
197
|
```
|
|
156
198
|
|
|
157
|
-
The registered `--ios-theme-split-pane-width` defaults to `320px`; `.ios-theme-split-pane-half-open` sets it to `50vw`. Set `halfOpened` when `
|
|
199
|
+
The registered `--ios-theme-split-pane-width` defaults to `320px`; `.ios-theme-split-pane-half-open` sets it to `50vw`. Set `halfOpened` when `foldStateChange` reports `state === 'half-opened'` (and read the initial value with `getFoldState()`). Ionic's `when` decides whether the menu is a persistent side pane; use the 900px breakpoint for a half-opened state or a flat state with hinge geometry, and the ordinary 992px breakpoint when closed or flat without geometry. Missing `hingeBounds` alone does not mean the device is flat. The application chooses where to apply this width rule; an ordinary split pane elsewhere is unchanged. This layout does not enable Vertical Bars or move an overlay menu.
|
|
158
200
|
|
|
159
201
|
## Vertical Control Area API
|
|
160
202
|
|
|
@@ -232,10 +274,11 @@ Stops synchronization, restores Web controls and releases native resources.
|
|
|
232
274
|
|
|
233
275
|
#### VerticalBarPlacement
|
|
234
276
|
|
|
235
|
-
| Prop
|
|
236
|
-
|
|
|
237
|
-
| **`edge`**
|
|
238
|
-
| **`inset`**
|
|
277
|
+
| Prop | Type | Description |
|
|
278
|
+
| ---------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
279
|
+
| **`edge`** | <code><a href="#verticalbaredge">VerticalBarEdge</a></code> | |
|
|
280
|
+
| **`inset`** | <code>number</code> | Explicit rail width in CSS pixels; omitted to use the stylesheet's safe-area rules. |
|
|
281
|
+
| **`nativeEdge`** | <code><a href="#verticalbaredge">VerticalBarEdge</a></code> | Native logical edge reported by the application's device plugin. Null or an unregistered edge uses a Web rail in verticalBarsOnly mode, or the ordinary Native UI Shell layout otherwise. Omission keeps the last supplied value. |
|
|
239
282
|
|
|
240
283
|
|
|
241
284
|
#### NativeUIShellStatus
|
package/docs/migration.md
CHANGED
|
@@ -8,8 +8,8 @@ title: Migration
|
|
|
8
8
|
|
|
9
9
|
Use `withNativeUIShellTransition()` to keep your existing Ionic navigation animation while coordinating Native UI Shell controls.
|
|
10
10
|
|
|
11
|
-
- If you already
|
|
12
|
-
- If you use Ionic's default animation without a `navAnimation` option, follow [Keep Ionic's default animation](./iphone-duo-with-original-theme.md#keep-
|
|
11
|
+
- If you already import `iosTransitionAnimation` from `@rdlabo/ionic-theme-ios27`, keep the [package animation setup](./iphone-duo-with-original-theme.md#use-this-package%27s-ios-animation); no configuration change is needed. It now uses the shared adapter internally; do not add another wrapper.
|
|
12
|
+
- If you use Ionic's default animation without a `navAnimation` option, follow [Keep Ionic's default animation](./iphone-duo-with-original-theme.md#keep-ionic%27s-default-animation). The example selects Ionic's standard iOS or MD builder from the transition mode.
|
|
13
13
|
- If you use a custom navigation animation with Native UI Shell or the standalone Vertical Control Area, wrap your existing builder when configuring Ionic:
|
|
14
14
|
|
|
15
15
|
```diff
|
|
@@ -25,7 +25,7 @@ Merge this option into your existing Ionic configuration before initialization.
|
|
|
25
25
|
|
|
26
26
|
Use the adapter only for navigation; leave modal and popover animations unchanged. Your builder must create a fresh `Animation` for each navigation because Ionic destroys it afterward. Keep lifecycle events for control registration and transitions without animation. If the custom builder animates a horizontal back button separately, exclude that effect while `.ios-theme-vertical-bars` is active.
|
|
27
27
|
|
|
28
|
-
See [Connect your navigation animation](./iphone-duo-with-original-theme.md#3
|
|
28
|
+
See [Connect your navigation animation](./iphone-duo-with-original-theme.md#3.-connect-your-navigation-animation) for the setup and supported scope.
|
|
29
29
|
|
|
30
30
|
## From the iOS 26 theme
|
|
31
31
|
|
package/docs/native-ui-shell.md
CHANGED
|
@@ -40,6 +40,8 @@ Native appearance follows the applied class, system or always-dark theme CSS. Sy
|
|
|
40
40
|
|
|
41
41
|
## Supported markup
|
|
42
42
|
|
|
43
|
+
The table below describes the ordinary Native UI Shell. Vertical Bars uses the separate [toolbar action rules](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo#toolbar-actions).
|
|
44
|
+
|
|
43
45
|
| Ionic component | Supported appearance and placement | Native rendering |
|
|
44
46
|
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
45
47
|
| `ion-button` | `fill="default"`, standard glass, in a fixed header/footer toolbar | Glass `UIButton` |
|
|
@@ -62,11 +64,11 @@ Use `ios-theme-shell-disabled` to disable only the iOS Native UI Shell while kee
|
|
|
62
64
|
|
|
63
65
|
If a child inside a shared native surface opts out, the entire surface stays on the Web: this includes button groups, tab bars, segments and FAB lists. Opting out of the search FAB or any part of the search footer disables native search integration; the tab bar can still render natively if it remains eligible.
|
|
64
66
|
|
|
65
|
-
Placement is required even when the appearance is glass. In the ordinary Native UI Shell, buttons, back buttons, menu-button groups and segments need a toolbar directly inside `ion-header` or `ion-footer`, with no `ion-content` ancestor around the control. Buttons directly inside a header/footer, standalone toolbars, and toolbars or headers nested in scrolling content stay on the Web. FABs without `slot="fixed"` also stay on the Web. Moving a projected control to an excluded location restores its Web rendering; moving it back re-evaluates eligibility. When `.ios-theme-vertical-bars` is enabled, a standard `ion-back-button` can instead be projected to the Vertical Control Area from outside a fixed toolbar, including routed content or a persistent app shell. The application chooses where to enable this mode and which Ionic component mode to use; Vertical Bars projection does not require `ios` mode classes.
|
|
67
|
+
Placement is required even when the appearance is glass. In the ordinary Native UI Shell, buttons, back buttons, menu-button groups and segments need a toolbar directly inside `ion-header` or `ion-footer`, with no `ion-content` ancestor around the control. Buttons directly inside a header/footer, standalone toolbars, and toolbars or headers nested in scrolling content stay on the Web. FABs without `slot="fixed"` also stay on the Web. Moving a projected control to an excluded location restores its Web rendering; moving it back re-evaluates eligibility. When `.ios-theme-vertical-bars` is enabled, a standard `ion-back-button` can instead be projected to the Vertical Control Area from outside a fixed toolbar, including routed content or a persistent app shell. The application chooses where to enable this mode and which Ionic component mode to use; Vertical Bars projection does not require `ios` mode classes. Collapsed headers, opted-out controls and departed pages are excluded. Full-width foreground modals can participate in Vertical Bars as described below; other overlay surfaces keep their own layout.
|
|
66
68
|
|
|
67
69
|
Native tabs accept equal-width items with Ionic's default `layout="icon-top"`. The native bar uses a local compact horizontal and regular vertical size class to preserve the Web's stacked icon/label arrangement on iPad and in landscape. This does not change the app's size class. Label size and weight follow the Web snapshot. Other explicit Ionic layouts (`icon-start`, `icon-end`, `icon-bottom`, `icon-hide`, `label-hide`) and unequal item widths keep the entire tab bar on the Web. Start, center and end placement follow the original `ion-tab-bar`, including RTL. Directional `ion-icon` artwork preserves its rendered RTL flip.
|
|
68
70
|
|
|
69
|
-
|
|
71
|
+
Outside Vertical Bars, standalone clear, solid and outline buttons are excluded. A glass `ion-buttons` group of two or more clear buttons is projected as one surface; its children retain separate actions. Menu buttons can also share that group. A single menu button uses its parent `ion-buttons` as the glass surface, so no Web glass is left underneath the native button. A menu button outside this theme glass remains on the Web. Mixed fills, unsupported children, or a theme-disabled child keep the group on the Web. Single clear buttons remain on the Web. Custom button colors, custom back icons/colors, collapsing headers, toolbars inside scrolling content, modal content, scrollable/expanded segments and segment-view integration remain on the Web. Complex slots and unsupported SVG features also fall back to Web. The plugin does not translate arbitrary application CSS into UIKit styles.
|
|
70
72
|
|
|
71
73
|
Native glass samples the Web content actually drawn behind it. Existing toolbar backgrounds and header blur still affect that content. For content to scroll beneath a header, use Ionic's normal translucent-header/fullscreen-content layout; the plugin does not move page content or override an application-owned opaque toolbar background.
|
|
72
74
|
|
|
@@ -116,7 +118,7 @@ FAB support covers the standard circular glass appearance, text and resolved sta
|
|
|
116
118
|
|
|
117
119
|
Custom host animation or transition declarations on the FAB, list or button keep the group on the Web until removed. Button transforms support the standard identity and hidden-child scale(0), not custom scaling. For children inside a `display:none` list, browsers can report a computed transform of `none` even when a custom transform is declared. Such transforms are checked when layout becomes available; the whole FAB then returns to Web if needed. The plugin does not parse application stylesheets or temporarily open lists to predict hidden layout.
|
|
118
120
|
|
|
119
|
-
The built-in `iosTransitionAnimation` waits for native retirement before starting the Web animation. Interactive progress and completion/cancellation are queued while that retirement is in progress. Stationary shared tabs are retained. First render and transitions without an animation builder are covered by the startup runtime and Ionic lifecycle events. Default Ionic and custom navigation builders can use the same integration through `withNativeUIShellTransition()`, exported from both the package root and `/vertical-bars`. See [Connect your navigation animation](./iphone-duo-with-original-theme.md#3
|
|
121
|
+
The built-in `iosTransitionAnimation` waits for native retirement before starting the Web animation. Interactive progress and completion/cancellation are queued while that retirement is in progress. Stationary shared tabs are retained. First render and transitions without an animation builder are covered by the startup runtime and Ionic lifecycle events. Default Ionic and custom navigation builders can use the same integration through `withNativeUIShellTransition()`, exported from both the package root and `/vertical-bars`. See [Connect your navigation animation](./iphone-duo-with-original-theme.md#3.-connect-your-navigation-animation) for the setup.
|
|
120
122
|
|
|
121
123
|
Tab switches skip the Web/native crossfade so a retiring UIKit snapshot cannot linger over the next tab. Detection uses the router URL versus the still-selected tab on `ionViewWillLeave` (and vanilla `ionTabsWillChange` / `ionTabsDidChange` DOM events). Stack pushes and pops keep the normal 180ms handoff.
|
|
122
124
|
|
|
@@ -175,9 +177,9 @@ The standalone Vertical Control Area entry point (`@rdlabo/ionic-theme-ios27/ver
|
|
|
175
177
|
|
|
176
178
|
On supported iOS versions, adding `.ios-theme-vertical-bars` changes only controls that the system relocates into the physical side rail. Native UI Shell presents eligible tabs, back navigation, menu buttons, and toolbar actions through a SwiftUI `TabView` and toolbar once the class is applied. When the OS reports a rail edge — on iPhone Duo linked against iOS 27.1 or later — it must agree with the applied placement; a disagreeing report keeps the rail on the Web. Older toolchains that cannot report an edge trust the DOM placement directly. SwiftUI owns their adaptive placement and Liquid Glass appearance; Ionic remains the source of labels, icons, selected/disabled state, routing, form submission, and click handlers.
|
|
177
179
|
|
|
178
|
-
The SwiftUI surface is clipped and hit-tested to the system rail. Web content remains visible and interactive outside that physical region. The runtime optimistically updates tab selection before forwarding the action to the original `ion-tab-button`, using the same event and stale-revision protection as the other native controls.
|
|
180
|
+
The SwiftUI surface is clipped and hit-tested to the system rail. Web content remains visible and interactive outside that physical region. The runtime optimistically updates tab selection before forwarding the action to the original `ion-tab-button`, using the same event and stale-revision protection as the other native controls. A topmost full-width modal supplies its own eligible toolbar actions and rail bounds, including when it is a sheet. Centered modals, menus, and popovers keep their own toolbar layout. Modal placement follows the visible dialog width rather than its type or hinge posture.
|
|
179
181
|
|
|
180
|
-
|
|
182
|
+
In Vertical Bars, fixed-toolbar `ion-button` actions use an `ion-icon` or SVG with `slot="icon-only"`. All fills (`default`, `clear`, `solid`, and `outline`) and Ionic colors are eligible; submit buttons follow the same placement rule. Solid actions retain their background color and outline actions retain their border color and width. Actions without `icon-only` stay horizontal. Scrolling and explicitly opted-out controls retain their Web presentation. Add `.ios-theme-horizontal-only` to an `ion-buttons` group or individual `ion-button` to keep it in the horizontal Web toolbar. On Web, Android, older iOS, or when native projection is unavailable during setup, the Web projection remains the fallback.
|
|
181
183
|
|
|
182
184
|
For standalone setup while keeping your existing theme, see [iPhone Duo with your existing theme](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme).
|
|
183
185
|
|
package/docs/special-markup.md
CHANGED
|
@@ -166,19 +166,6 @@ The example uses Ionic's standard collapsible large-title structure. Scroll the
|
|
|
166
166
|
|
|
167
167
|
The `.ion-page` wrapper makes this embedded preview behave like a complete routed page. An application using `ion-router-outlet` normally receives that page container automatically. The inset list and its items only provide enough content to demonstrate scrolling; they are not required by `.searchbar-classic`.
|
|
168
168
|
|
|
169
|
-
## Search-bar toolbars
|
|
170
|
-
|
|
171
|
-
Add `.toolbar-searchbar` when an `ion-toolbar` combines a search bar with start or end buttons. The class centers the slotted controls and adjusts the spacing around the search field.
|
|
172
|
-
|
|
173
|
-
```html preview
|
|
174
|
-
<ion-toolbar class="toolbar-searchbar">
|
|
175
|
-
<ion-buttons slot="start">
|
|
176
|
-
<ion-button>Cancel</ion-button>
|
|
177
|
-
</ion-buttons>
|
|
178
|
-
<ion-searchbar></ion-searchbar>
|
|
179
|
-
</ion-toolbar>
|
|
180
|
-
```
|
|
181
|
-
|
|
182
169
|
## Opting out
|
|
183
170
|
|
|
184
171
|
Add `.ios-theme-disabled` to an individual Ionic component when it must retain Ionic's standard iOS styling.
|
|
@@ -5,7 +5,7 @@ import UIKit
|
|
|
5
5
|
protocol ShellVerticalBarsControlling: AnyObject {
|
|
6
6
|
var view: UIView { get }
|
|
7
7
|
func attach(to owner: UIViewController, in parent: UIView)
|
|
8
|
-
func apply(_ controls: [ShellControl], rendering: ShellRendering, edge: String)
|
|
8
|
+
@MainActor func apply(_ controls: [ShellControl], rendering: ShellRendering, edge: String)
|
|
9
9
|
func detach()
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -16,6 +16,9 @@ final class ShellVerticalBarsModel: ObservableObject {
|
|
|
16
16
|
let label: String
|
|
17
17
|
let accessibilityLabel: String
|
|
18
18
|
let image: UIImage?
|
|
19
|
+
let background: Color?
|
|
20
|
+
let borderColor: Color?
|
|
21
|
+
let borderWidth: CGFloat
|
|
19
22
|
let badge: ShellBadge?
|
|
20
23
|
let disabled: Bool
|
|
21
24
|
var selected: Bool
|
|
@@ -41,7 +44,11 @@ final class ShellVerticalBarsModel: ObservableObject {
|
|
|
41
44
|
func item(_ source: ShellItem) -> Item {
|
|
42
45
|
Item(id: source.id, label: source.content.label,
|
|
43
46
|
accessibilityLabel: source.content.accessibilityLabel,
|
|
44
|
-
image: rendering.image(source.content),
|
|
47
|
+
image: rendering.image(source.content),
|
|
48
|
+
background: source.content.backgroundColor.map { Color(uiColor: rendering.color($0)) },
|
|
49
|
+
borderColor: source.content.borderColor.map { Color(uiColor: rendering.color($0)) },
|
|
50
|
+
borderWidth: source.content.borderWidth ?? 0,
|
|
51
|
+
badge: source.content.badge, disabled: source.content.disabled,
|
|
45
52
|
selected: source.content.selected)
|
|
46
53
|
}
|
|
47
54
|
withAnimation(.smooth(duration: 0.3)) {
|
|
@@ -99,11 +106,21 @@ private struct ShellVerticalBarsLabel: View {
|
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
@available(iOS 26.0, *)
|
|
109
|
+
@ViewBuilder
|
|
102
110
|
private func verticalBarsButton(_ item: ShellVerticalBarsModel.Item, model: ShellVerticalBarsModel) -> some View {
|
|
103
|
-
Button { model.activate(item.id) } label: { ShellVerticalBarsLabel(item: item) }
|
|
111
|
+
let button = Button { model.activate(item.id) } label: { ShellVerticalBarsLabel(item: item) }
|
|
104
112
|
.disabled(item.disabled)
|
|
105
113
|
.accessibilityLabel(item.accessibilityLabel)
|
|
106
114
|
.accessibilityIdentifier(item.id)
|
|
115
|
+
if let background = item.background {
|
|
116
|
+
button.buttonStyle(.glassProminent).tint(background)
|
|
117
|
+
} else {
|
|
118
|
+
button.overlay {
|
|
119
|
+
if let borderColor = item.borderColor {
|
|
120
|
+
Capsule().strokeBorder(borderColor, lineWidth: item.borderWidth).allowsHitTesting(false)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
107
124
|
}
|
|
108
125
|
|
|
109
126
|
@available(iOS 26.0, *)
|
|
@@ -340,10 +357,12 @@ final class ShellVerticalBarsController: ShellVerticalBarsControlling {
|
|
|
340
357
|
controller.didMove(toParent: owner)
|
|
341
358
|
}
|
|
342
359
|
|
|
343
|
-
func apply(_ controls: [ShellControl], rendering: ShellRendering, edge: String) {
|
|
360
|
+
@MainActor func apply(_ controls: [ShellControl], rendering: ShellRendering, edge: String) {
|
|
344
361
|
container.railEdge = edge
|
|
345
362
|
controller.railEdge = edge
|
|
363
|
+
let tabsAppear = model.tabs.isEmpty && controls.contains { $0.kind == .tabBar && !$0.items.isEmpty }
|
|
346
364
|
model.apply(controls, rendering: rendering)
|
|
365
|
+
if tabsAppear { ShellCrossfade.enter(container, duration: ShellCrossfade.duration(180)) }
|
|
347
366
|
controller.overrideUserInterfaceStyle = controls.contains(where: \.dark) ? .dark : .light
|
|
348
367
|
}
|
|
349
368
|
|