@erkamyaman/capacitor-foldable 8.4.1 → 8.4.3

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 CHANGED
@@ -1,3 +1,4 @@
1
+ <p align="center"><img src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/logo/logo.svg" width="168" height="129" alt="Capacitor Foldable logo"></p>
1
2
  <h1 align="center">Capacitor Foldable</h1>
2
3
  <p align="center"><strong><code>@erkamyaman/capacitor-foldable</code></strong></p>
3
4
  <p align="center">
@@ -11,58 +12,30 @@
11
12
  Fold state, posture, hinge angle, size classes and bar placement for foldable phones such as Galaxy Z Fold and Flip, Pixel Fold and iPhone Duo, in Capacitor apps.
12
13
  </p>
13
14
 
14
- <p align="center">
15
- <img height="240" alt="iPhone Duo closed, on the outer display" src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/iphone-duo-closed.png">
16
- <img height="240" alt="iPhone Duo half-open in book posture" src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/iphone-duo-book.png">
17
- <img height="240" alt="iPhone Duo fully open" src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/iphone-duo-open.png">
18
- </p>
19
- <p align="center">
20
- <img height="300" alt="Pixel Fold in tabletop posture" src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/android-tabletop.jpg">
21
- <img height="300" alt="Pixel Fold half-open in book posture" src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/android-book.jpg">
22
- </p>
23
-
24
- ```typescript
25
- const { state, hingeOrientation } = await Foldable.getFoldState();
26
- // { state: 'half-opened', hingeOrientation: 'horizontal' }
27
- ```
28
-
29
- ### Versions
15
+ <table align="center">
16
+ <tr>
17
+ <td align="center">📚 <a href="https://erkamyaman.github.io/capacitor-foldable/"><strong>Documentation</strong></a></td>
18
+ </tr>
19
+ </table>
30
20
 
31
- | Capacitor | Plugin | iOS |
32
- | --------- | ------ | --- |
33
- | v7.x | v7.x | Size classes only, build with Xcode 26 |
34
- | v8.x | v8.x | Size classes, plus iPhone Duo on Capacitor 8.5+ |
35
-
36
- ### Supported Platforms
37
-
38
- | Platform | Status | Minimum |
39
- | -------- | ------ | ------- |
40
- | Android | Supported | API 24, `compileSdk` 36 |
41
- | iOS | Size classes everywhere; fold, hinge and bar placement on iPhone Duo with Capacitor 8.5+ and iOS 27.1+, built with any Xcode | iOS 15 |
42
- | Web | Stub, returns `flat` | |
43
-
44
- ## Do you need this plugin?
45
-
46
- Web views already resize with the window, so a responsive layout that pads with every safe-area inset adapts to foldables without a plugin. Use this one when your app needs to know what CSS can't tell it:
47
-
48
- - **Whether the device folds at all**, and whether it can stand half-open like a laptop.
49
- - **Where the fold is**, and whether it splits the screen.
50
- - **How the device is held**: flat, tabletop or book.
51
- - **The hinge angle**, for effects and interactions.
52
- - **Outer or inner display**, and Material window size classes.
53
- - **Where iPhone Duo puts native bars**, so your HTML tab bar can follow. For Ionic's `ion-tabs` there's a ready-made stylesheet, plus a fix for the tab bar disappearing when you fold.
54
- - **The outer display on Android foldables**: rear display and dual screen.
55
-
56
- Google's [adaptive app quality guidelines](https://developer.android.com/develop/adaptive-apps/quality-guidelines/adaptive-app-quality) make posture support a Tier 1 check and size-class-driven layouts a Tier 2 one, and Apple's [iPhone Duo guidance](https://developer.apple.com/design/human-interface-guidelines/designing-for-iphone-duo) asks for layouts that adapt to the fold rather than to a device. This plugin reports what both are about.
21
+ | Ionic tabs on iPhone Duo without the plugin | With the plugin |
22
+ | --- | --- |
23
+ | <img src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/ionic-before-book.png" width="360" alt="Ionic tab bar across the fold in book posture"> | <img src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/ionic-after-book.png" width="360" alt="Ionic tab bar moved to the side in book posture"> |
57
24
 
58
- ## Installation
25
+ ## Install
59
26
 
60
27
  ```bash
61
28
  npm install @erkamyaman/capacitor-foldable
62
29
  npx cap sync
63
30
  ```
64
31
 
65
- On Capacitor 7, install `@erkamyaman/capacitor-foldable@7` instead.
32
+ | Capacitor | Plugin | Install | iOS |
33
+ | --------- | ------ | ------- | --- |
34
+ | v9 beta | v9.x beta | `npm install @erkamyaman/capacitor-foldable@next` | Size classes and iPhone Duo, iOS 16+. Setup in the [release notes](https://github.com/erkamyaman/capacitor-foldable/releases/tag/v9.0.0-beta.0) |
35
+ | v8.x | v8.x | `npm install @erkamyaman/capacitor-foldable` | Size classes, plus iPhone Duo on Capacitor 8.5+ and iOS 27.1, built with any Xcode |
36
+ | v7.x | v7.x | `npm install @erkamyaman/capacitor-foldable@latest-7` | Size classes only |
37
+
38
+ Android works on API 24+ with any foldable Jetpack WindowManager supports. The web build is a stub that reports `flat`.
66
39
 
67
40
  ## Usage
68
41
 
@@ -76,533 +49,32 @@ Foldable.addListener('foldStateChange', ({ posture }) => {
76
49
  });
77
50
  ```
78
51
 
79
- More in [Examples](docs/examples.md).
52
+ Prefer CSS? `installFoldablePolyfill()` fills in the Device Posture and Viewport Segments APIs that Capacitor's web views turn off, and sets `--fold-*` variables and classes on `<html>`. Using `ion-tabs`? `import '@erkamyaman/capacitor-foldable/ionic-tabs.css'`.
80
53
 
81
- ## Web standard APIs
54
+ **Everything else is in the [documentation](https://erkamyaman.github.io/capacitor-foldable/).**
82
55
 
83
- Chrome ships the [Device Posture API](https://developer.mozilla.org/docs/Web/API/Device_Posture_API) and the [Viewport Segments API](https://developer.mozilla.org/docs/Web/API/Viewport_Segments_API), but Android's WebView turns both off. Safari 27.1 has them behind a feature flag that is off by default and belongs to Safari, so a `WKWebView` inside your app cannot switch it on. Neither works in a Capacitor app today. `installFoldablePolyfill()` fills them in from the native fold state, and mirrors the CSS features as classes and variables on `<html>`:
84
-
85
- ```typescript
86
- import { installFoldablePolyfill } from '@erkamyaman/capacitor-foldable';
87
-
88
- await installFoldablePolyfill();
89
-
90
- navigator.devicePosture.type; // 'continuous' | 'folded'
91
- window.viewport.segments; // two DOMRects when the fold splits the web view
92
- ```
93
-
94
- | Standard CSS | With the polyfill |
95
- | --- | --- |
96
- | `@media (device-posture: folded)` | `.device-posture-folded` |
97
- | `@media (horizontal-viewport-segments: 2)` | `.horizontal-viewport-segments-2` |
98
- | `@media (vertical-viewport-segments: 2)` | `.vertical-viewport-segments-2` |
99
- | `env(viewport-segment-width 0 0)` | `var(--viewport-segment-width-0-0)` |
100
- | Native bars moved to the side (iPhone Duo) | `.vertical-bars-leading`, `.vertical-bars-trailing` |
101
- | The phone is being folded right now | `.folding` |
102
-
103
- The segment variables only exist while the device is half-open. To lay out along the fold whether it's flat or half-open, such as a game on one side and its controls on the other, the polyfill also sets the fold's position whenever there is one:
104
-
105
- | Fold | With the polyfill |
106
- | --- | --- |
107
- | Direction | `.fold-vertical`, `.fold-horizontal` |
108
- | Position and size | `var(--fold-left)`, `var(--fold-top)`, `var(--fold-width)`, `var(--fold-height)` |
109
-
110
- ```css
111
- .fold-vertical .game { width: var(--fold-left); }
112
- .fold-vertical .controls { left: calc(var(--fold-left) + var(--fold-width)); }
113
- ```
114
-
115
- Using Ionic's `ion-tabs`? `import '@erkamyaman/capacitor-foldable/ionic-tabs.css'` moves it to the side the way native tab bars do on iPhone Duo. See [Ionic tabs](docs/iphone-duo.md#ionic-tabs).
116
-
117
- | Ionic tabs without the plugin | With `ionic-tabs.css` |
118
- | --- | --- |
119
- | <img src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/ionic-before-book.png" width="300" alt="Ionic tab bar across the fold in book posture"> | <img src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/ionic-after-book.png" width="300" alt="Ionic tab bar as a pill on the side in book posture"> |
120
- | <img src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/ionic-before-closed.png" width="220" alt="Ionic tab bar at the bottom of the closed iPhone Duo"> | <img src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/ionic-after-closed.png" width="220" alt="Ionic tab bar as a pill under the clock on the closed iPhone Duo"> |
121
-
122
- ## Apps built with it
123
-
124
- - **[Hinge Guess](https://github.com/erkamyaman/hinge-guess)** is a small game: fold the phone to a target angle and score how close you got. Ionic Angular, native tabs, and the plugin behind the dial.
125
- - **[examples](examples)** in this repo is a catalogue of one-file demos, one per API, laid out as two pages around the crease on iPhone Duo.
126
- - **[example-app](example-app)** is the plain single-screen app that shows every value the plugin reports at once.
127
-
128
- ## Store review
129
-
130
- The plugin is built to give store review nothing to object to.
131
-
132
- **iOS.** Public APIs only, UIKit and Capacitor, with no private symbols. An app built against an SDK older than 27.1 reaches the iOS 27.1 APIs through the Objective-C runtime, since the compiler cannot see them, and they are the same public APIs either way. It touches none of Apple's [required reason APIs](https://developer.apple.com/documentation/bundleresources/describing-use-of-required-reason-api), collects nothing and tracks nobody, and it ships a `PrivacyInfo.xcprivacy` saying exactly that, which Xcode folds into your app's privacy report. Nothing it uses needs a purpose string in `Info.plist`. Any Xcode that Capacitor 8 supports builds fine, and reports the fold on a 27.1 device either way.
133
-
134
- **Android.** The plugin adds no permissions and no `uses-feature` to your app, so it changes nothing about which devices can install it. Its own manifest is empty; Jetpack WindowManager merges two optional `<uses-library>` entries, which are how it binds to the OEM window extensions and filter nothing. The hinge angle comes from `TYPE_HINGE_ANGLE`, which needs no permission, and rear display and dual screen go through `WindowAreaController`, also permission free. `minSdk` is 24, the same as Capacitor's own default, and `targetSdk` follows your project.
135
-
136
- **Data safety and privacy labels.** Everything stays on the device: the plugin has no network code and stores nothing. There is no entry for it to add to Play's Data safety form or to your App Store privacy label.
56
+ ## Guides
137
57
 
138
- ## Skill for coding agents
58
+ - [API reference](https://erkamyaman.github.io/capacitor-foldable/api.html)
59
+ - [iPhone Duo](https://erkamyaman.github.io/capacitor-foldable/iphone-duo.html)
60
+ - [Layout tips](https://erkamyaman.github.io/capacitor-foldable/layout-tips.html)
61
+ - [Examples](https://erkamyaman.github.io/capacitor-foldable/examples.html)
62
+ - [Testing on Android](https://erkamyaman.github.io/capacitor-foldable/testing-android.html)
63
+ - [Store review](https://erkamyaman.github.io/capacitor-foldable/store-review.html)
139
64
 
140
- The repo ships an agent skill, so Claude Code and other agents know how to use the plugin instead of guessing at the API:
65
+ ## Agent skill
141
66
 
142
67
  ```bash
143
68
  npx skills add erkamyaman/capacitor-foldable@capacitor-foldable
144
69
  ```
145
70
 
146
- It is one skill with the platform detail split out: laying out around the crease in CSS, the full API, Ionic specifics, iPhone Duo, Android foldables, and how to test on both. It is beta, so tell me if any of it steers you wrong. It also ships inside the npm package under [`skills/`](skills/capacitor-foldable), so an agent working in a project that depends on the plugin can find it in `node_modules`.
147
-
148
- The same skill is published on its own at [iphone-duo-capacitor-skills](https://github.com/erkamyaman/iphone-duo-capacitor-skills), which is where it sits next to the iPhone Duo skills for SwiftUI and UIKit.
149
-
150
- ## Guides
151
-
152
- - [Examples](docs/examples.md)
153
- - [Layout tips](docs/layout-tips.md)
154
- - [Testing on Android](docs/testing-android.md)
155
- - [iPhone Duo](docs/iphone-duo.md)
156
-
157
- ## API
158
-
159
- <docgen-index>
160
-
161
- * [`isDeviceFoldable()`](#isdevicefoldable)
162
- * [`getFoldState()`](#getfoldstate)
163
- * [`getHingeAngle()`](#gethingeangle)
164
- * [`getReservedRegions()`](#getreservedregions)
165
- * [`setVerticalBarBehavior(...)`](#setverticalbarbehavior)
166
- * [`getSizeClass()`](#getsizeclass)
167
- * [`getDisplayModes()`](#getdisplaymodes)
168
- * [`getBarPlacement()`](#getbarplacement)
169
- * [`startRearDisplay()`](#startreardisplay)
170
- * [`stopRearDisplay()`](#stopreardisplay)
171
- * [`startDualScreen(...)`](#startdualscreen)
172
- * [`stopDualScreen()`](#stopdualscreen)
173
- * [`addListener('foldStateChange', ...)`](#addlistenerfoldstatechange-)
174
- * [`addListener('hingeAngleChange', ...)`](#addlistenerhingeanglechange-)
175
- * [`addListener('sizeClassChange', ...)`](#addlistenersizeclasschange-)
176
- * [`addListener('displayModeChange', ...)`](#addlistenerdisplaymodechange-)
177
- * [`addListener('foldingChange', ...)`](#addlistenerfoldingchange-)
178
- * [`addListener('barPlacementChange', ...)`](#addlistenerbarplacementchange-)
179
- * [Interfaces](#interfaces)
180
- * [Type Aliases](#type-aliases)
181
-
182
- </docgen-index>
183
-
184
- <docgen-api>
185
- <!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
186
-
187
- ### isDeviceFoldable()
188
-
189
- ```typescript
190
- isDeviceFoldable() => Promise<{ foldable: boolean; supportsTabletop: boolean; }>
191
- ```
192
-
193
- Whether the device has a fold at all, and whether it can be propped half
194
- open like a laptop. Both `false` on web, and on iOS except on iPhone Duo
195
- (iOS 27.1 or later).
196
-
197
- **Returns:** <code>Promise&lt;{ foldable: boolean; supportsTabletop: boolean; }&gt;</code>
198
-
199
- **Since:** 7.0.0
200
-
201
- --------------------
202
-
203
-
204
- ### getFoldState()
205
-
206
- ```typescript
207
- getFoldState() => Promise<FoldState>
208
- ```
209
-
210
- Read the current fold state. Resolves to
211
- `{ state: 'flat', isSeparating: false, posture: 'flat' }` when there is no
212
- fold information.
213
-
214
- **Returns:** <code>Promise&lt;<a href="#foldstate">FoldState</a>&gt;</code>
215
-
216
- **Since:** 7.0.0
217
-
218
- --------------------
219
-
220
-
221
- ### getHingeAngle()
222
-
223
- ```typescript
224
- getHingeAngle() => Promise<{ angle: number | null; status: HingeStatus | null; }>
225
- ```
226
-
227
- Read the angle between the two halves of the device, in degrees: `0` when
228
- closed, `180` when flat. Resolves to `{ angle: null }` on devices without a
229
- hinge angle sensor, on web, and on iOS before 27.1.
230
-
231
- **Returns:** <code>Promise&lt;{ angle: number | null; status: <a href="#hingestatus">HingeStatus</a> | null; }&gt;</code>
232
-
233
- **Since:** 7.0.0
234
-
235
- --------------------
236
-
237
-
238
- ### getReservedRegions()
239
-
240
- ```typescript
241
- getReservedRegions() => Promise<{ regions: ReservedRegion[]; }>
242
- ```
243
-
244
- Read every region the system reserves on this display: the fold and
245
- anything covering the screen, active or not. iPhone Duo reports the fold,
246
- the vertical status bar area and the under-display camera. Resolves to an
247
- empty list on Android, on web and on iOS before 27.1.
248
-
249
- **Returns:** <code>Promise&lt;{ regions: ReservedRegion[]; }&gt;</code>
250
-
251
- **Since:** 8.2.0
252
-
253
- --------------------
254
-
255
-
256
- ### setVerticalBarBehavior(...)
257
-
258
- ```typescript
259
- setVerticalBarBehavior(options: { behavior: 'automatic' | 'disabled'; }) => Promise<{ applied: boolean; }>
260
- ```
261
-
262
- Choose whether iPhone Duo may move this app's bars to the side of the
263
- display. `'disabled'` keeps everything horizontal, including the status
264
- bar. Resolves to `{ applied: false }` when the app does not use
265
- `FoldableBridgeViewController`, on Android, on web and on iOS before 27.1.
266
- See the iPhone Duo guide for the one-line storyboard change.
267
-
268
- | Param | Type |
269
- | ------------- | ----------------------------------------------------- |
270
- | **`options`** | <code>{ behavior: 'automatic' \| 'disabled'; }</code> |
271
-
272
- **Returns:** <code>Promise&lt;{ applied: boolean; }&gt;</code>
273
-
274
- **Since:** 8.2.0
275
-
276
- --------------------
277
-
278
-
279
- ### getSizeClass()
280
-
281
- ```typescript
282
- getSizeClass() => Promise<SizeClass>
283
- ```
284
-
285
- Read the window's size classes: Apple's compact and regular, the signal its
286
- iPhone Duo guidelines recommend for telling the outer display from the inner
287
- one, and Material's width and height classes. On iOS `horizontal` and
288
- `vertical` are UIKit's size classes; everything else comes from the window
289
- size.
290
-
291
- **Returns:** <code>Promise&lt;<a href="#sizeclass">SizeClass</a>&gt;</code>
292
-
293
- **Since:** 7.0.0
294
-
295
- --------------------
296
-
297
-
298
- ### getDisplayModes()
299
-
300
- ```typescript
301
- getDisplayModes() => Promise<DisplayModes>
302
- ```
303
-
304
- Read which of the foldable display modes the device offers right now. Both
305
- are `'unsupported'` on iOS and web.
306
-
307
- **Returns:** <code>Promise&lt;<a href="#displaymodes">DisplayModes</a>&gt;</code>
308
-
309
- **Since:** 7.0.0
310
-
311
- --------------------
312
-
313
-
314
- ### getBarPlacement()
315
-
316
- ```typescript
317
- getBarPlacement() => Promise<BarPlacement>
318
- ```
319
-
320
- Read where native bars go. Native tab bars and toolbars move to the side of
321
- the display on iPhone Duo, but HTML ones stay put, so use this to move your
322
- own tab bar too.
323
-
324
- **Returns:** <code>Promise&lt;<a href="#barplacement">BarPlacement</a>&gt;</code>
325
-
326
- **Since:** 7.0.0
327
-
328
- --------------------
329
-
330
-
331
- ### startRearDisplay()
332
-
333
- ```typescript
334
- startRearDisplay() => Promise<void>
335
- ```
336
-
337
- Move the app to the outer display. Android asks the user to confirm first,
338
- and the promise resolves once the app has moved. Rejects when rear display
339
- mode is not `'available'`. Only on Android.
340
-
341
- **Since:** 7.0.0
342
-
343
- --------------------
344
-
345
-
346
- ### stopRearDisplay()
347
-
348
- ```typescript
349
- stopRearDisplay() => Promise<void>
350
- ```
351
-
352
- Move the app back to the inner display.
353
-
354
- **Since:** 7.0.0
355
-
356
- --------------------
357
-
358
-
359
- ### startDualScreen(...)
360
-
361
- ```typescript
362
- startDualScreen(options: { url: string; }) => Promise<void>
363
- ```
364
-
365
- Show a page on the outer display while the app stays on the inner one. A
366
- relative `url` resolves against the app's own URL, so `'cover.html'` loads a
367
- page bundled with the app. The page runs in its own web view without access
368
- to Capacitor plugins. Calling this again while dual-screen mode is active
369
- replaces the page. Only on Android.
370
-
371
- | Param | Type |
372
- | ------------- | ----------------------------- |
373
- | **`options`** | <code>{ url: string; }</code> |
374
-
375
- **Since:** 7.0.0
376
-
377
- --------------------
378
-
379
-
380
- ### stopDualScreen()
381
-
382
- ```typescript
383
- stopDualScreen() => Promise<void>
384
- ```
385
-
386
- Close the page on the outer display.
387
-
388
- **Since:** 7.0.0
389
-
390
- --------------------
391
-
392
-
393
- ### addListener('foldStateChange', ...)
394
-
395
- ```typescript
396
- addListener(eventName: 'foldStateChange', listenerFunc: (state: FoldState) => void) => Promise<PluginListenerHandle>
397
- ```
398
-
399
- Listen for fold state changes. On a foldable this also fires when the
400
- device rotates, because `hingeOrientation` and `hingeBounds` rotate with
401
- the window.
402
-
403
- | Param | Type |
404
- | ------------------ | ------------------------------------------------------------------- |
405
- | **`eventName`** | <code>'foldStateChange'</code> |
406
- | **`listenerFunc`** | <code>(state: <a href="#foldstate">FoldState</a>) =&gt; void</code> |
407
-
408
- **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
409
-
410
- **Since:** 7.0.0
411
-
412
- --------------------
413
-
414
-
415
- ### addListener('hingeAngleChange', ...)
416
-
417
- ```typescript
418
- addListener(eventName: 'hingeAngleChange', listenerFunc: (event: { angle: number; }) => void) => Promise<PluginListenerHandle>
419
- ```
420
-
421
- Listen for hinge angle changes. On Android the hinge sensor only runs while
422
- at least one of these listeners is registered. On iOS it fires on iPhone
423
- Duo (iOS 27.1 or later). Never fires on web.
424
-
425
- | Param | Type |
426
- | ------------------ | --------------------------------------------------- |
427
- | **`eventName`** | <code>'hingeAngleChange'</code> |
428
- | **`listenerFunc`** | <code>(event: { angle: number; }) =&gt; void</code> |
429
-
430
- **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
431
-
432
- **Since:** 7.0.0
433
-
434
- --------------------
435
-
436
-
437
- ### addListener('sizeClassChange', ...)
438
-
439
- ```typescript
440
- addListener(eventName: 'sizeClassChange', listenerFunc: (sizeClass: SizeClass) => void) => Promise<PluginListenerHandle>
441
- ```
442
-
443
- Listen for size class changes, such as unfolding the device, rotating it or
444
- resizing the window. On iOS this fires when UIKit's size classes change
445
- (iOS 17 or later) or the device rotates, so a resize that keeps the same
446
- size classes may not update `widthClass` and `heightClass` until then.
447
-
448
- | Param | Type |
449
- | ------------------ | ----------------------------------------------------------------------- |
450
- | **`eventName`** | <code>'sizeClassChange'</code> |
451
- | **`listenerFunc`** | <code>(sizeClass: <a href="#sizeclass">SizeClass</a>) =&gt; void</code> |
452
-
453
- **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
454
-
455
- **Since:** 7.0.0
456
-
457
- --------------------
458
-
459
-
460
- ### addListener('displayModeChange', ...)
461
-
462
- ```typescript
463
- addListener(eventName: 'displayModeChange', listenerFunc: (modes: DisplayModes) => void) => Promise<PluginListenerHandle>
464
- ```
465
-
466
- Listen for changes to the display modes, including a mode ending because
467
- the user folded or unfolded the device. Never fires on iOS and web.
468
-
469
- | Param | Type |
470
- | ------------------ | ------------------------------------------------------------------------- |
471
- | **`eventName`** | <code>'displayModeChange'</code> |
472
- | **`listenerFunc`** | <code>(modes: <a href="#displaymodes">DisplayModes</a>) =&gt; void</code> |
473
-
474
- **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
475
-
476
- **Since:** 7.0.0
477
-
478
- --------------------
479
-
480
-
481
- ### addListener('foldingChange', ...)
482
-
483
- ```typescript
484
- addListener(eventName: 'foldingChange', listenerFunc: (event: { folding: boolean; }) => void) => Promise<PluginListenerHandle>
485
- ```
486
-
487
- Listen for the device being folded or unfolded. `folding` turns `true` as
488
- soon as the hinge starts moving and `false` about half a second after it
489
- stops, so an app can pause animations or heavy work while the screen is in
490
- motion. Only fires where a hinge angle is available: Android foldables with
491
- a hinge sensor, and iPhone Duo on iOS 27.1 or later.
492
-
493
- | Param | Type |
494
- | ------------------ | ------------------------------------------------------ |
495
- | **`eventName`** | <code>'foldingChange'</code> |
496
- | **`listenerFunc`** | <code>(event: { folding: boolean; }) =&gt; void</code> |
497
-
498
- **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
499
-
500
- **Since:** 8.2.0
501
-
502
- --------------------
503
-
504
-
505
- ### addListener('barPlacementChange', ...)
506
-
507
- ```typescript
508
- addListener(eventName: 'barPlacementChange', listenerFunc: (placement: BarPlacement) => void) => Promise<PluginListenerHandle>
509
- ```
510
-
511
- Listen for bar placement changes, such as opening or rotating iPhone Duo.
512
- Only fires on iOS 27.1 or later.
513
-
514
- | Param | Type |
515
- | ------------------ | ----------------------------------------------------------------------------- |
516
- | **`eventName`** | <code>'barPlacementChange'</code> |
517
- | **`listenerFunc`** | <code>(placement: <a href="#barplacement">BarPlacement</a>) =&gt; void</code> |
518
-
519
- **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
520
-
521
- **Since:** 7.0.0
522
-
523
- --------------------
524
-
525
-
526
- ### Interfaces
527
-
528
-
529
- #### FoldState
530
-
531
- | Prop | Type | Description | Since |
532
- | ---------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
533
- | **`state`** | <code>'flat' \| 'half-opened' \| 'closed'</code> | Posture of the fold. `'closed'` is never reported today: a device shut on its cover display reports `'flat'`. | 7.0.0 |
534
- | **`isSeparating`** | <code>boolean</code> | Whether the fold splits the web view into two areas: `true` when half-opened, or when the hinge has a physical gap. | 7.0.0 |
535
- | **`posture`** | <code>'flat' \| 'tabletop' \| 'book'</code> | How the device is held: `'tabletop'` when half-opened with a horizontal hinge, like a laptop, `'book'` when half-opened with a vertical hinge, and `'flat'` otherwise. | 7.0.0 |
536
- | **`hingeOrientation`** | <code>'horizontal' \| 'vertical'</code> | Direction of the hinge relative to the window, so it flips when the device rotates. Omitted when there is no fold. | 7.0.0 |
537
- | **`hingeBounds`** | <code>{ x: number; y: number; width: number; height: number; }</code> | Position of the fold in CSS pixels, relative to the web view. Zero wide (or zero tall) on a seamless fold. Omitted when there is no fold. | 7.0.0 |
538
- | **`occludedBounds`** | <code>{ x: number; y: number; width: number; height: number; }</code> | Area of the web view the hinge covers, in CSS pixels. Only present on devices with a physical gap. | 7.0.0 |
539
- | **`activeDisplay`** | <code>'inner' \| 'outer'</code> | Which display is showing the app, on a device with an inner and an outer display such as iPhone Duo. Reported on iPhone Duo (iOS 27.1 or later) and omitted elsewhere. | 7.0.0 |
540
- | **`cameraBounds`** | <code>{ x: number; y: number; width: number; height: number; }[]</code> | Areas of the web view something covers, in CSS pixels. On Android these are the display cutouts. On iPhone Duo these are the active occlusion regions: the camera in use, and the strip the system keeps for the vertical status bar. Use `getReservedRegions()` when you need to tell them apart. Omitted when there are none. | 7.0.0 |
541
- | **`hingeMargins`** | <code>{ top: number; right: number; bottom: number; left: number; }</code> | Space the system asks you to keep clear around the fold, in CSS pixels. `hingeBounds` covers the crease plus these margins, so the crease itself is `hingeBounds` shrunk by them. iPhone Duo reports 20 on each side of a vertical fold. Omitted on Android and where there is no fold. | 8.2.0 |
542
-
543
-
544
- #### ReservedRegion
545
-
546
- | Prop | Type | Description | Since |
547
- | -------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----- |
548
- | **`kind`** | <code>'division' \| 'occlusion'</code> | `'division'` is the fold itself. `'occlusion'` is something covering the display, such as a camera or the vertical status bar area. | 8.2.0 |
549
- | **`isActive`** | <code>boolean</code> | Whether the region applies right now. An inactive division is a flat fold, and an inactive occlusion is a camera that is not in use. | 8.2.0 |
550
- | **`x`** | <code>number</code> | | |
551
- | **`y`** | <code>number</code> | | |
552
- | **`width`** | <code>number</code> | | |
553
- | **`height`** | <code>number</code> | | |
554
- | **`margins`** | <code>{ top: number; right: number; bottom: number; left: number; }</code> | Space to keep clear around the region, in CSS pixels. | 8.2.0 |
555
-
556
-
557
- #### SizeClass
558
-
559
- | Prop | Type | Description | Since |
560
- | ----------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
561
- | **`horizontal`** | <code>'compact' \| 'regular'</code> | Width size class of the window: `'compact'` on a phone and on the outer display of a foldable, `'regular'` on the inner display, tablets and wide windows. On Android and web `'regular'` starts at 600 CSS pixels. | 7.0.0 |
562
- | **`vertical`** | <code>'compact' \| 'regular'</code> | Height size class of the window: `'compact'` on a phone in landscape. On Android and web `'regular'` starts at 480 CSS pixels. | 7.0.0 |
563
- | **`widthClass`** | <code>'compact' \| 'medium' \| 'expanded' \| 'large' \| 'extraLarge'</code> | Material window width class, from the window width in CSS pixels: `'compact'` below 600, `'medium'` below 840, `'expanded'` below 1200, `'large'` below 1600 and `'extraLarge'` from 1600. | 7.0.0 |
564
- | **`heightClass`** | <code>'compact' \| 'medium' \| 'expanded'</code> | Material window height class, from the window height in CSS pixels: `'compact'` below 480, `'medium'` below 900 and `'expanded'` from 900. | 7.0.0 |
565
-
566
-
567
- #### DisplayModes
568
-
569
- | Prop | Type | Description | Since |
570
- | ----------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
571
- | **`rearDisplay`** | <code><a href="#displaymodestatus">DisplayModeStatus</a></code> | Rear display mode moves the app to the outer display, so people can frame a photo with the rear cameras. Only on Android foldables that offer it. | 7.0.0 |
572
- | **`dualScreen`** | <code><a href="#displaymodestatus">DisplayModeStatus</a></code> | Dual-screen mode shows a second page on the outer display while the app stays on the inner one. Only on Android foldables that offer it. | 7.0.0 |
573
-
574
-
575
- #### BarPlacement
576
-
577
- | Prop | Type | Description | Since |
578
- | --------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
579
- | **`verticalBarEdge`** | <code>'leading' \| 'trailing' \| null</code> | The edge iPhone Duo moves tab bars and toolbars to when it lays them out vertically: `'leading'` or `'trailing'` in the reading direction, so `'leading'` is the left edge in left-to-right languages. `null` when bars stay horizontal, and always on Android, web and iOS before 27.1. An app built with an older Xcode still gets the edge: it is inferred from the safe-area inset the bar leaves, since the trait that names it is missing. | 7.0.0 |
580
- | **`inset`** | <code>number</code> | How much room the vertical bar takes on its edge, in points, rounded to whole points. It is the same value the web view reports as a safe-area inset, so `env(safe-area-inset-*)` already covers simple layouts. Read it when you place your own chrome and need the number itself, and do not assume it stays at 84: it differs when the bar is disabled, and Apple can change it. `0` when no vertical bar is reported, and always on Android and web. | 8.3.3 |
581
-
582
-
583
- #### PluginListenerHandle
584
-
585
- | Prop | Type |
586
- | ------------ | ----------------------------------------- |
587
- | **`remove`** | <code>() =&gt; Promise&lt;void&gt;</code> |
588
-
589
-
590
- ### Type Aliases
591
-
592
-
593
- #### HingeStatus
594
-
595
- How far open the hinge is, as the system sees it. Note that it can lag the
596
- angle: iOS keeps reporting `'closed'` for a moment after the phone opens.
597
-
598
- <code>'closed' | 'partiallyOpen' | 'fullyOpen'</code>
599
-
600
-
601
- #### DisplayModeStatus
71
+ Also ships in the npm package under [`skills/`](skills/capacitor-foldable), and on its own at [iphone-duo-capacitor-skills](https://github.com/erkamyaman/iphone-duo-capacitor-skills).
602
72
 
603
- <code>'unsupported' | 'unavailable' | 'available' | 'active'</code>
73
+ ## Used by
604
74
 
605
- </docgen-api>
75
+ - [Capacitor Native UI](https://github.com/rdlabo-dev/ionic-theme-ios27/tree/main/packages/capacitor-native-ui) and [Ionic Theme iOS 27](https://github.com/rdlabo-dev/ionic-theme-ios27) by rdlabo
76
+ - [RecipeSage](https://github.com/julianpoy/RecipeSage), a collaborative recipe keeper and meal planner, moves its native tab bar to the side on iPhone Duo with it
77
+ - [Hinge Guess](https://github.com/erkamyaman/hinge-guess), a game where the phone's hinge is the controller
606
78
 
607
79
  ## Trademarks and licence
608
80
 
@@ -31,6 +31,8 @@ public class FoldablePlugin: CAPPlugin, CAPBridgedPlugin {
31
31
  private var lastHingeAngle: Double?
32
32
  private var isFolding = false
33
33
  private var foldingWorkItem: DispatchWorkItem?
34
+ private var barPlacementWorkItems: [DispatchWorkItem] = []
35
+ private var barPlacementNeedsResend = false
34
36
  private var orientationObserver: NSObjectProtocol?
35
37
 
36
38
  @objc override public func load() {
@@ -103,7 +105,12 @@ public class FoldablePlugin: CAPPlugin, CAPBridgedPlugin {
103
105
  @objc func getBarPlacement(_ call: CAPPluginCall) {
104
106
  DispatchQueue.main.async { [weak self] in
105
107
  guard let self = self else { return }
106
- call.resolve(self.currentBarPlacement())
108
+ let placement = self.currentBarPlacement()
109
+ if let last = self.lastBarPlacement, !(last as NSDictionary).isEqual(to: placement) {
110
+ self.barPlacementNeedsResend = true
111
+ self.notifyBarPlacementAfterSettling()
112
+ }
113
+ call.resolve(placement)
107
114
  }
108
115
  }
109
116
 
@@ -153,6 +160,7 @@ public class FoldablePlugin: CAPPlugin, CAPBridgedPlugin {
153
160
  implementation.observeChanges(in: view) { [weak self] in
154
161
  self?.notifyFoldStateNowAndAfterSettling()
155
162
  self?.notifyHingeAngleIfChanged()
163
+ self?.notifyBarPlacementAfterSettling()
156
164
  }
157
165
 
158
166
  UIDevice.current.beginGeneratingDeviceOrientationNotifications()
@@ -163,7 +171,7 @@ public class FoldablePlugin: CAPPlugin, CAPBridgedPlugin {
163
171
  ) { [weak self] _ in
164
172
  self?.notifyFoldStateNowAndAfterSettling()
165
173
  self?.notifySizeClassIfChanged()
166
- self?.notifyBarPlacementIfChanged()
174
+ self?.notifyBarPlacementAfterSettling()
167
175
  }
168
176
 
169
177
  guard #available(iOS 17.0, *) else { return }
@@ -179,7 +187,7 @@ public class FoldablePlugin: CAPPlugin, CAPBridgedPlugin {
179
187
  _ = view.registerForTraitChanges(traits) { [weak self] (_: UIView, _: UITraitCollection) in
180
188
  self?.notifySizeClassIfChanged()
181
189
  self?.notifyFoldStateNowAndAfterSettling()
182
- self?.notifyBarPlacementIfChanged()
190
+ self?.notifyBarPlacementAfterSettling()
183
191
  }
184
192
  }
185
193
  }
@@ -211,12 +219,22 @@ public class FoldablePlugin: CAPPlugin, CAPBridgedPlugin {
211
219
 
212
220
  private func notifyBarPlacementIfChanged() {
213
221
  let placement = currentBarPlacement()
214
- if let last = lastBarPlacement, (last as NSDictionary).isEqual(to: placement) { return }
222
+ if !barPlacementNeedsResend, let last = lastBarPlacement, (last as NSDictionary).isEqual(to: placement) { return }
215
223
 
224
+ barPlacementNeedsResend = false
216
225
  lastBarPlacement = placement
217
226
  notifyListeners("barPlacementChange", data: placement)
218
227
  }
219
228
 
229
+ private func notifyBarPlacementAfterSettling() {
230
+ barPlacementWorkItems.forEach { $0.cancel() }
231
+ barPlacementWorkItems = [0.25, 0.8].map { delay in
232
+ let check = DispatchWorkItem { [weak self] in self?.notifyBarPlacementIfChanged() }
233
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: check)
234
+ return check
235
+ }
236
+ }
237
+
220
238
  private func notifyFoldStateNowAndAfterSettling() {
221
239
  notifyFoldStateIfChanged()
222
240
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erkamyaman/capacitor-foldable",
3
- "version": "8.4.1",
3
+ "version": "8.4.3",
4
4
  "description": "Fold-state and dual-display APIs for foldable devices in Capacitor apps",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -75,7 +75,7 @@
75
75
  "eslint": "eslint . --ext ts",
76
76
  "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
77
77
  "swiftlint": "node-swiftlint",
78
- "docgen": "docgen --api FoldablePlugin --output-readme README.md --output-json dist/docs.json",
78
+ "docgen": "node scripts/docs-api.mjs open && docgen --api FoldablePlugin --output-readme docs/api.md --output-json dist/docs.json && node scripts/docs-api.mjs close",
79
79
  "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
80
80
  "clean": "rimraf ./dist",
81
81
  "watch": "tsc --watch",
@@ -23,16 +23,21 @@ React and Vue import it from the entry file as usual, since their bundlers ship
23
23
 
24
24
  ## Projecting Ionic controls into native UI
25
25
 
26
- Moving the tab bar with CSS is one answer. The other is [`@rdlabo/ionic-theme-ios27`](https://github.com/rdlabo-dev/ionic-theme-ios27), which brings SwiftUI-like UI rearrangement to Ionic apps: it repositions existing `ion-back-button`, `ion-buttons`, `ion-button` and `ion-tab-bar` controls, then projects them into native UI. An app can adopt its Vertical Bars while keeping its existing theme.
26
+ Moving the tab bar with CSS is one answer. The other is [`@rdlabo/capacitor-native-ui`](https://github.com/rdlabo-dev/ionic-theme-ios27/tree/main/packages/capacitor-native-ui), which renders existing `ion-back-button`, `ion-buttons`, `ion-button`, `ion-tab-bar` and segment controls with UIKit and SwiftUI while the original elements, state and handlers stay in the WebView. It can put them in the system's vertical rail on iPhone Duo, and it works with an app's existing theme. The optional [`@rdlabo/ionic-theme-ios27`](https://github.com/rdlabo-dev/ionic-theme-ios27) adds iOS 27 styling on top.
27
27
 
28
- The split is clean: this plugin provides device state, the theme handles rearrangement and rendering. The app passes what it reads here into the theme, and the theme decides what to draw.
28
+ The split is clean: this plugin provides device state, Native UI projects the controls, and the theme handles Web styling. Both rdlabo packages list this plugin in their standard setup, installed by the app as a direct dependency:
29
29
 
30
- For setup, send the reader to the theme's own documentation rather than duplicating it here:
30
+ ```sh
31
+ npm install @rdlabo/capacitor-native-ui @erkamyaman/capacitor-foldable
32
+ ```
33
+
34
+ For setup, send the reader to rdlabo's own documentation rather than duplicating it here:
31
35
 
32
- - [iPhone Duo with your existing theme](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme)
33
- - [Vertical Bars reference](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars)
36
+ - [Native UI guide](https://github.com/rdlabo-dev/ionic-theme-ios27/tree/main/packages/capacitor-native-ui)
37
+ - [Vertical Bars with an existing Ionic theme](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/capacitor-native-ui/docs/ionic-vertical-bars.md)
38
+ - [Migrating from theme 1.2.x](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/capacitor-native-ui/docs/migration.md), where the native APIs were still bundled in the theme
34
39
 
35
- Use the CSS stylesheet above when you want Ionic's own tab bar moved, and the theme when you want the controls rendered natively.
40
+ Use the CSS stylesheet above when you want Ionic's own tab bar moved, and Native UI when you want the controls rendered natively.
36
41
 
37
42
  ## A fold fires a keyboard event
38
43