@erkamyaman/capacitor-foldable 8.1.0 → 8.3.0

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.
Files changed (36) hide show
  1. package/ErkamyamanCapacitorFoldable.podspec +1 -0
  2. package/Package.swift +2 -1
  3. package/README.md +131 -12
  4. package/android/src/main/kotlin/io/github/erkamyaman/foldable/FoldablePlugin.kt +31 -1
  5. package/dist/docs.json +176 -4
  6. package/dist/esm/css.d.ts +6 -1
  7. package/dist/esm/css.js +16 -2
  8. package/dist/esm/css.js.map +1 -1
  9. package/dist/esm/definitions.d.ts +95 -4
  10. package/dist/esm/definitions.js.map +1 -1
  11. package/dist/esm/polyfill.js +6 -1
  12. package/dist/esm/polyfill.js.map +1 -1
  13. package/dist/esm/web.d.ts +7 -0
  14. package/dist/esm/web.js +7 -1
  15. package/dist/esm/web.js.map +1 -1
  16. package/dist/plugin.cjs.js +29 -4
  17. package/dist/plugin.cjs.js.map +1 -1
  18. package/dist/plugin.js +29 -4
  19. package/dist/plugin.js.map +1 -1
  20. package/ionic-tabs.css +2 -1
  21. package/ios/Sources/FoldablePlugin/Foldable.swift +41 -1
  22. package/ios/Sources/FoldablePlugin/FoldableBridgeViewController.swift +41 -0
  23. package/ios/Sources/FoldablePlugin/FoldablePlugin.swift +41 -0
  24. package/ios/Sources/FoldablePlugin/ReservedRegionFoldProvider.swift +41 -11
  25. package/ios/Sources/FoldablePlugin/Resources/PrivacyInfo.xcprivacy +14 -0
  26. package/ios/Sources/FoldablePlugin/UIKitFoldSource.swift +3 -3
  27. package/ios/Tests/FoldablePluginTests/FoldableTests.swift +4 -0
  28. package/ios/Tests/FoldablePluginTests/ReservedRegionFoldProviderTests.swift +18 -0
  29. package/package.json +7 -6
  30. package/skills/capacitor-foldable/SKILL.md +87 -0
  31. package/skills/capacitor-foldable/references/android.md +52 -0
  32. package/skills/capacitor-foldable/references/api.md +112 -0
  33. package/skills/capacitor-foldable/references/css-layout.md +127 -0
  34. package/skills/capacitor-foldable/references/ionic.md +50 -0
  35. package/skills/capacitor-foldable/references/iphone-duo.md +54 -0
  36. package/skills/capacitor-foldable/references/testing.md +53 -0
@@ -11,6 +11,7 @@ Pod::Spec.new do |s|
11
11
  s.author = package['author']
12
12
  s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
13
13
  s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
14
+ s.resource_bundles = { 'ErkamyamanCapacitorFoldablePrivacy' => ['ios/Sources/FoldablePlugin/Resources/PrivacyInfo.xcprivacy'] }
14
15
  s.ios.deployment_target = '14.0'
15
16
  s.dependency 'Capacitor'
16
17
  s.swift_version = '5.1'
package/Package.swift CHANGED
@@ -19,7 +19,8 @@ let package = Package(
19
19
  .product(name: "Capacitor", package: "capacitor-swift-pm"),
20
20
  .product(name: "Cordova", package: "capacitor-swift-pm")
21
21
  ],
22
- path: "ios/Sources/FoldablePlugin"),
22
+ path: "ios/Sources/FoldablePlugin",
23
+ resources: [.copy("Resources/PrivacyInfo.xcprivacy")]),
23
24
  .testTarget(
24
25
  name: "FoldablePluginTests",
25
26
  dependencies: ["FoldablePlugin"],
package/README.md CHANGED
@@ -53,6 +53,8 @@ Web views already resize with the window, so a responsive layout that pads with
53
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
54
  - **The outer display on Android foldables**: rear display and dual screen.
55
55
 
56
+ Google's [adaptive app quality guidelines](https://developer.android.com/develop/adaptive-apps/quality-guidelines/adaptive-app-quality) put window size classes and posture support in Tier 1, 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 checklists are about.
57
+
56
58
  ## Installation
57
59
 
58
60
  ```bash
@@ -96,6 +98,7 @@ window.viewport.segments; // two DOMRects when the fold splits the web view
96
98
  | `@media (vertical-viewport-segments: 2)` | `.vertical-viewport-segments-2` |
97
99
  | `env(viewport-segment-width 0 0)` | `var(--viewport-segment-width-0-0)` |
98
100
  | Native bars moved to the side (iPhone Duo) | `.vertical-bars-leading`, `.vertical-bars-trailing` |
101
+ | The phone is being folded right now | `.folding` |
99
102
 
100
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:
101
104
 
@@ -116,6 +119,32 @@ Using Ionic's `ion-tabs`? `import '@erkamyaman/capacitor-foldable/ionic-tabs.css
116
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"> |
117
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"> |
118
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 so it cannot be the reason an app is rejected.
131
+
132
+ **iOS.** Public APIs only, UIKit and Capacitor, with no dynamic lookup or private symbols. It touches none of Apple's [required reason APIs](https://developer.apple.com/documentation/bundleresources/privacy-manifest-files), 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`. The iOS 27.1 code is behind a compile-time SDK check, so older Xcode versions build fine.
133
+
134
+ **Android.** The plugin's manifest is empty: it adds no permissions to your app. The hinge angle comes from `TYPE_HINGE_ANGLE`, which needs no permission, and it is registered at `SENSOR_DELAY_NORMAL`, well under the rate that would require `HIGH_SAMPLING_RATE_SENSORS`. Rear display and dual screen go through Jetpack WindowManager's `WindowAreaController`, also permission free. `minSdk` is 24 and `targetSdk` follows your project, so it never holds you back from Play's target API rules.
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.
137
+
138
+ ## Skill for coding agents
139
+
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:
141
+
142
+ ```bash
143
+ npx skills add erkamyaman/capacitor-foldable@capacitor-foldable
144
+ ```
145
+
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
+
119
148
  ## Guides
120
149
 
121
150
  - [Examples](docs/examples.md)
@@ -130,6 +159,8 @@ Using Ionic's `ion-tabs`? `import '@erkamyaman/capacitor-foldable/ionic-tabs.css
130
159
  * [`isDeviceFoldable()`](#isdevicefoldable)
131
160
  * [`getFoldState()`](#getfoldstate)
132
161
  * [`getHingeAngle()`](#gethingeangle)
162
+ * [`getReservedRegions()`](#getreservedregions)
163
+ * [`setVerticalBarBehavior(...)`](#setverticalbarbehavior)
133
164
  * [`getSizeClass()`](#getsizeclass)
134
165
  * [`getDisplayModes()`](#getdisplaymodes)
135
166
  * [`getBarPlacement()`](#getbarplacement)
@@ -141,6 +172,7 @@ Using Ionic's `ion-tabs`? `import '@erkamyaman/capacitor-foldable/ionic-tabs.css
141
172
  * [`addListener('hingeAngleChange', ...)`](#addlistenerhingeanglechange-)
142
173
  * [`addListener('sizeClassChange', ...)`](#addlistenersizeclasschange-)
143
174
  * [`addListener('displayModeChange', ...)`](#addlistenerdisplaymodechange-)
175
+ * [`addListener('foldingChange', ...)`](#addlistenerfoldingchange-)
144
176
  * [`addListener('barPlacementChange', ...)`](#addlistenerbarplacementchange-)
145
177
  * [Interfaces](#interfaces)
146
178
  * [Type Aliases](#type-aliases)
@@ -187,20 +219,61 @@ fold information.
187
219
  ### getHingeAngle()
188
220
 
189
221
  ```typescript
190
- getHingeAngle() => Promise<{ angle: number | null; }>
222
+ getHingeAngle() => Promise<{ angle: number | null; status: HingeStatus | null; }>
191
223
  ```
192
224
 
193
225
  Read the angle between the two halves of the device, in degrees: `0` when
194
226
  closed, `180` when flat. Resolves to `{ angle: null }` on devices without a
195
227
  hinge angle sensor, on web, and on iOS before 27.1.
196
228
 
197
- **Returns:** <code>Promise&lt;{ angle: number | null; }&gt;</code>
229
+ **Returns:** <code>Promise&lt;{ angle: number | null; status: <a href="#hingestatus">HingeStatus</a> | null; }&gt;</code>
198
230
 
199
231
  **Since:** 7.0.0
200
232
 
201
233
  --------------------
202
234
 
203
235
 
236
+ ### getReservedRegions()
237
+
238
+ ```typescript
239
+ getReservedRegions() => Promise<{ regions: ReservedRegion[]; }>
240
+ ```
241
+
242
+ Read every region the system reserves on this display: the fold and
243
+ anything covering the screen, active or not. iPhone Duo reports the fold,
244
+ the vertical status bar area and the under-display camera. Resolves to an
245
+ empty list on Android, on web and on iOS before 27.1.
246
+
247
+ **Returns:** <code>Promise&lt;{ regions: ReservedRegion[]; }&gt;</code>
248
+
249
+ **Since:** 8.2.0
250
+
251
+ --------------------
252
+
253
+
254
+ ### setVerticalBarBehavior(...)
255
+
256
+ ```typescript
257
+ setVerticalBarBehavior(options: { behavior: 'automatic' | 'disabled'; }) => Promise<{ applied: boolean; }>
258
+ ```
259
+
260
+ Choose whether iPhone Duo may move this app's bars to the side of the
261
+ display. `'disabled'` keeps everything horizontal, including the status
262
+ bar. Resolves to `{ applied: false }` when the app does not use
263
+ `FoldableBridgeViewController`, on Android, on web and on iOS before 27.1.
264
+ See the iPhone Duo guide for the one-line storyboard change.
265
+
266
+ | Param | Type |
267
+ | ------------- | ----------------------------------------------------- |
268
+ | **`options`** | <code>{ behavior: 'automatic' \| 'disabled'; }</code> |
269
+
270
+ **Returns:** <code>Promise&lt;{ applied: boolean; }&gt;</code>
271
+
272
+ **Since:** 8.2.0
273
+
274
+ --------------------
275
+
276
+
204
277
  ### getSizeClass()
205
278
 
206
279
  ```typescript
@@ -403,6 +476,30 @@ the user folded or unfolded the device. Never fires on iOS and web.
403
476
  --------------------
404
477
 
405
478
 
479
+ ### addListener('foldingChange', ...)
480
+
481
+ ```typescript
482
+ addListener(eventName: 'foldingChange', listenerFunc: (event: { folding: boolean; }) => void) => Promise<PluginListenerHandle>
483
+ ```
484
+
485
+ Listen for the device being folded or unfolded. `folding` turns `true` as
486
+ soon as the hinge starts moving and `false` about half a second after it
487
+ stops, so an app can pause animations or heavy work while the screen is in
488
+ motion. Only fires where a hinge angle is available: Android foldables with
489
+ a hinge sensor, and iPhone Duo on iOS 27.1 or later.
490
+
491
+ | Param | Type |
492
+ | ------------------ | ------------------------------------------------------ |
493
+ | **`eventName`** | <code>'foldingChange'</code> |
494
+ | **`listenerFunc`** | <code>(event: { folding: boolean; }) =&gt; void</code> |
495
+
496
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
497
+
498
+ **Since:** 8.2.0
499
+
500
+ --------------------
501
+
502
+
406
503
  ### addListener('barPlacementChange', ...)
407
504
 
408
505
  ```typescript
@@ -429,16 +526,30 @@ Only fires on iOS 27.1 or later.
429
526
 
430
527
  #### FoldState
431
528
 
432
- | Prop | Type | Description | Since |
433
- | ---------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
434
- | **`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 |
435
- | **`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 |
436
- | **`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 |
437
- | **`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 |
438
- | **`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 |
439
- | **`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 |
440
- | **`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 |
441
- | **`cameraBounds`** | <code>{ x: number; y: number; width: number; height: number; }[]</code> | Areas of the web view covered by a front-facing camera, in CSS pixels. On Android these are the display cutouts. On iPhone Duo they are the outer camera and the under-display inner camera while it is in use. Omitted when there are none. | 7.0.0 |
529
+ | Prop | Type | Description | Since |
530
+ | ---------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
531
+ | **`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 |
532
+ | **`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 |
533
+ | **`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 |
534
+ | **`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 |
535
+ | **`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 |
536
+ | **`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 |
537
+ | **`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 |
538
+ | **`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 |
539
+ | **`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 |
540
+
541
+
542
+ #### ReservedRegion
543
+
544
+ | Prop | Type | Description | Since |
545
+ | -------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----- |
546
+ | **`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 |
547
+ | **`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 |
548
+ | **`x`** | <code>number</code> | | |
549
+ | **`y`** | <code>number</code> | | |
550
+ | **`width`** | <code>number</code> | | |
551
+ | **`height`** | <code>number</code> | | |
552
+ | **`margins`** | <code>{ top: number; right: number; bottom: number; left: number; }</code> | Space to keep clear around the region, in CSS pixels. | 8.2.0 |
442
553
 
443
554
 
444
555
  #### SizeClass
@@ -476,6 +587,14 @@ Only fires on iOS 27.1 or later.
476
587
  ### Type Aliases
477
588
 
478
589
 
590
+ #### HingeStatus
591
+
592
+ How far open the hinge is, as the system sees it. Note that it can lag the
593
+ angle: iOS keeps reporting `'closed'` for a moment after the phone opens.
594
+
595
+ <code>'closed' | 'partiallyOpen' | 'fullyOpen'</code>
596
+
597
+
479
598
  #### DisplayModeStatus
480
599
 
481
600
  <code>'unsupported' | 'unavailable' | 'available' | 'active'</code>
@@ -39,6 +39,8 @@ class FoldablePlugin : Plugin() {
39
39
  private var displayModes: DisplayModes? = null
40
40
  private var lastKnownState: FoldState? = null
41
41
  private var hingeAngleJob: Job? = null
42
+ private var foldingJob: Job? = null
43
+ private var isFolding = false
42
44
  private var lastKnownAngle: Float? = null
43
45
  private var lastSizeClass: SizeClass? = null
44
46
  private var lastDisplayModes: DisplayModeStatuses? = null
@@ -106,6 +108,16 @@ class FoldablePlugin : Plugin() {
106
108
  )
107
109
  }
108
110
 
111
+ @PluginMethod
112
+ fun setVerticalBarBehavior(call: PluginCall) {
113
+ call.resolve(JSObject().put("applied", false))
114
+ }
115
+
116
+ @PluginMethod
117
+ fun getReservedRegions(call: PluginCall) {
118
+ call.resolve(JSObject().put("regions", JSArray()))
119
+ }
120
+
109
121
  @PluginMethod
110
122
  fun getFoldState(call: PluginCall) {
111
123
  lastKnownState?.let {
@@ -234,7 +246,7 @@ class FoldablePlugin : Plugin() {
234
246
  }
235
247
 
236
248
  private fun updateHingeAngleUpdates() {
237
- if (!hasListeners(HINGE_ANGLE_CHANGE)) {
249
+ if (!hasListeners(HINGE_ANGLE_CHANGE) && !hasListeners(FOLDING_CHANGE)) {
238
250
  hingeAngleJob?.cancel()
239
251
  hingeAngleJob = null
240
252
  lastKnownAngle = null
@@ -250,6 +262,7 @@ class FoldablePlugin : Plugin() {
250
262
  implementation.hingeAngles().conflate().collect { angle ->
251
263
  lastKnownAngle = angle
252
264
  notifyListeners(HINGE_ANGLE_CHANGE, angleResult(angle))
265
+ notifyFolding()
253
266
  delay(HINGE_ANGLE_FRAME_MS)
254
267
  }
255
268
  } finally {
@@ -259,6 +272,21 @@ class FoldablePlugin : Plugin() {
259
272
  }
260
273
  }
261
274
 
275
+ /** `true` while the hinge keeps moving, `false` once it has been still for a moment. */
276
+ private fun notifyFolding() {
277
+ if (!isFolding) {
278
+ isFolding = true
279
+ notifyListeners(FOLDING_CHANGE, JSObject().put("folding", true))
280
+ }
281
+
282
+ foldingJob?.cancel()
283
+ foldingJob = scope.launch {
284
+ delay(FOLDING_SETTLE_MS)
285
+ isFolding = false
286
+ notifyListeners(FOLDING_CHANGE, JSObject().put("folding", false))
287
+ }
288
+ }
289
+
262
290
  @SuppressLint("SetJavaScriptEnabled")
263
291
  private fun dualScreenWebView(context: Context, url: String): WebView = WebView(context).apply {
264
292
  settings.javaScriptEnabled = true
@@ -326,6 +354,8 @@ class FoldablePlugin : Plugin() {
326
354
  private companion object {
327
355
  const val FIRST_EMISSION_TIMEOUT_MS = 1_000L
328
356
  const val HINGE_ANGLE_CHANGE = "hingeAngleChange"
357
+ const val FOLDING_CHANGE = "foldingChange"
358
+ const val FOLDING_SETTLE_MS = 500L
329
359
  const val HINGE_ANGLE_FRAME_MS = 16L
330
360
  const val SIZE_CLASS_CHANGE = "sizeClassChange"
331
361
  const val DISPLAY_MODE_CHANGE = "displayModeChange"
package/dist/docs.json CHANGED
@@ -39,9 +39,9 @@
39
39
  },
40
40
  {
41
41
  "name": "getHingeAngle",
42
- "signature": "() => Promise<{ angle: number | null; }>",
42
+ "signature": "() => Promise<{ angle: number | null; status: HingeStatus | null; }>",
43
43
  "parameters": [],
44
- "returns": "Promise<{ angle: number | null; }>",
44
+ "returns": "Promise<{ angle: number | null; status: HingeStatus | null; }>",
45
45
  "tags": [
46
46
  {
47
47
  "name": "since",
@@ -49,9 +49,49 @@
49
49
  }
50
50
  ],
51
51
  "docs": "Read the angle between the two halves of the device, in degrees: `0` when\nclosed, `180` when flat. Resolves to `{ angle: null }` on devices without a\nhinge angle sensor, on web, and on iOS before 27.1.",
52
- "complexTypes": [],
52
+ "complexTypes": [
53
+ "HingeStatus"
54
+ ],
53
55
  "slug": "gethingeangle"
54
56
  },
57
+ {
58
+ "name": "getReservedRegions",
59
+ "signature": "() => Promise<{ regions: ReservedRegion[]; }>",
60
+ "parameters": [],
61
+ "returns": "Promise<{ regions: ReservedRegion[]; }>",
62
+ "tags": [
63
+ {
64
+ "name": "since",
65
+ "text": "8.2.0"
66
+ }
67
+ ],
68
+ "docs": "Read every region the system reserves on this display: the fold and\nanything covering the screen, active or not. iPhone Duo reports the fold,\nthe vertical status bar area and the under-display camera. Resolves to an\nempty list on Android, on web and on iOS before 27.1.",
69
+ "complexTypes": [
70
+ "ReservedRegion"
71
+ ],
72
+ "slug": "getreservedregions"
73
+ },
74
+ {
75
+ "name": "setVerticalBarBehavior",
76
+ "signature": "(options: { behavior: 'automatic' | 'disabled'; }) => Promise<{ applied: boolean; }>",
77
+ "parameters": [
78
+ {
79
+ "name": "options",
80
+ "docs": "",
81
+ "type": "{ behavior: 'automatic' | 'disabled'; }"
82
+ }
83
+ ],
84
+ "returns": "Promise<{ applied: boolean; }>",
85
+ "tags": [
86
+ {
87
+ "name": "since",
88
+ "text": "8.2.0"
89
+ }
90
+ ],
91
+ "docs": "Choose whether iPhone Duo may move this app's bars to the side of the\ndisplay. `'disabled'` keeps everything horizontal, including the status\nbar. Resolves to `{ applied: false }` when the app does not use\n`FoldableBridgeViewController`, on Android, on web and on iOS before 27.1.\nSee the iPhone Duo guide for the one-line storyboard change.",
92
+ "complexTypes": [],
93
+ "slug": "setverticalbarbehavior"
94
+ },
55
95
  {
56
96
  "name": "getSizeClass",
57
97
  "signature": "() => Promise<SizeClass>",
@@ -284,6 +324,34 @@
284
324
  ],
285
325
  "slug": "addlistenerdisplaymodechange-"
286
326
  },
327
+ {
328
+ "name": "addListener",
329
+ "signature": "(eventName: 'foldingChange', listenerFunc: (event: { folding: boolean; }) => void) => Promise<PluginListenerHandle>",
330
+ "parameters": [
331
+ {
332
+ "name": "eventName",
333
+ "docs": "",
334
+ "type": "'foldingChange'"
335
+ },
336
+ {
337
+ "name": "listenerFunc",
338
+ "docs": "",
339
+ "type": "(event: { folding: boolean; }) => void"
340
+ }
341
+ ],
342
+ "returns": "Promise<PluginListenerHandle>",
343
+ "tags": [
344
+ {
345
+ "name": "since",
346
+ "text": "8.2.0"
347
+ }
348
+ ],
349
+ "docs": "Listen for the device being folded or unfolded. `folding` turns `true` as\nsoon as the hinge starts moving and `false` about half a second after it\nstops, so an app can pause animations or heavy work while the screen is in\nmotion. Only fires where a hinge angle is available: Android foldables with\na hinge sensor, and iPhone Duo on iOS 27.1 or later.",
350
+ "complexTypes": [
351
+ "PluginListenerHandle"
352
+ ],
353
+ "slug": "addlistenerfoldingchange-"
354
+ },
287
355
  {
288
356
  "name": "addListener",
289
357
  "signature": "(eventName: 'barPlacementChange', listenerFunc: (placement: BarPlacement) => void) => Promise<PluginListenerHandle>",
@@ -416,9 +484,94 @@
416
484
  "name": "since"
417
485
  }
418
486
  ],
419
- "docs": "Areas of the web view covered by a front-facing camera, in CSS pixels. On\nAndroid these are the display cutouts. On iPhone Duo they are the outer\ncamera and the under-display inner camera while it is in use. Omitted when\nthere are none.",
487
+ "docs": "Areas of the web view something covers, in CSS pixels. On Android these are\nthe display cutouts. On iPhone Duo these are the active occlusion regions:\nthe camera in use, and the strip the system keeps for the vertical status\nbar. Use `getReservedRegions()` when you need to tell them apart. Omitted\nwhen there are none.",
420
488
  "complexTypes": [],
421
489
  "type": "{ x: number; y: number; width: number; height: number; }[] | undefined"
490
+ },
491
+ {
492
+ "name": "hingeMargins",
493
+ "tags": [
494
+ {
495
+ "text": "8.2.0",
496
+ "name": "since"
497
+ }
498
+ ],
499
+ "docs": "Space the system asks you to keep clear around the fold, in CSS pixels.\n`hingeBounds` covers the crease plus these margins, so the crease itself is\n`hingeBounds` shrunk by them. iPhone Duo reports 20 on each side of a\nvertical fold. Omitted on Android and where there is no fold.",
500
+ "complexTypes": [],
501
+ "type": "{ top: number; right: number; bottom: number; left: number; } | undefined"
502
+ }
503
+ ]
504
+ },
505
+ {
506
+ "name": "ReservedRegion",
507
+ "slug": "reservedregion",
508
+ "docs": "",
509
+ "tags": [],
510
+ "methods": [],
511
+ "properties": [
512
+ {
513
+ "name": "kind",
514
+ "tags": [
515
+ {
516
+ "text": "8.2.0",
517
+ "name": "since"
518
+ }
519
+ ],
520
+ "docs": "`'division'` is the fold itself. `'occlusion'` is something covering the\ndisplay, such as a camera or the vertical status bar area.",
521
+ "complexTypes": [],
522
+ "type": "'division' | 'occlusion'"
523
+ },
524
+ {
525
+ "name": "isActive",
526
+ "tags": [
527
+ {
528
+ "text": "8.2.0",
529
+ "name": "since"
530
+ }
531
+ ],
532
+ "docs": "Whether the region applies right now. An inactive division is a flat fold,\nand an inactive occlusion is a camera that is not in use.",
533
+ "complexTypes": [],
534
+ "type": "boolean"
535
+ },
536
+ {
537
+ "name": "x",
538
+ "tags": [],
539
+ "docs": "",
540
+ "complexTypes": [],
541
+ "type": "number"
542
+ },
543
+ {
544
+ "name": "y",
545
+ "tags": [],
546
+ "docs": "",
547
+ "complexTypes": [],
548
+ "type": "number"
549
+ },
550
+ {
551
+ "name": "width",
552
+ "tags": [],
553
+ "docs": "",
554
+ "complexTypes": [],
555
+ "type": "number"
556
+ },
557
+ {
558
+ "name": "height",
559
+ "tags": [],
560
+ "docs": "",
561
+ "complexTypes": [],
562
+ "type": "number"
563
+ },
564
+ {
565
+ "name": "margins",
566
+ "tags": [
567
+ {
568
+ "text": "8.2.0",
569
+ "name": "since"
570
+ }
571
+ ],
572
+ "docs": "Space to keep clear around the region, in CSS pixels.",
573
+ "complexTypes": [],
574
+ "type": "{ top: number; right: number; bottom: number; left: number; }"
422
575
  }
423
576
  ]
424
577
  },
@@ -556,6 +709,25 @@
556
709
  ],
557
710
  "enums": [],
558
711
  "typeAliases": [
712
+ {
713
+ "name": "HingeStatus",
714
+ "slug": "hingestatus",
715
+ "docs": "How far open the hinge is, as the system sees it. Note that it can lag the\nangle: iOS keeps reporting `'closed'` for a moment after the phone opens.",
716
+ "types": [
717
+ {
718
+ "text": "'closed'",
719
+ "complexTypes": []
720
+ },
721
+ {
722
+ "text": "'partiallyOpen'",
723
+ "complexTypes": []
724
+ },
725
+ {
726
+ "text": "'fullyOpen'",
727
+ "complexTypes": []
728
+ }
729
+ ]
730
+ },
559
731
  {
560
732
  "name": "DisplayModeStatus",
561
733
  "slug": "displaymodestatus",
package/dist/esm/css.d.ts CHANGED
@@ -3,7 +3,12 @@ export declare const CSS_CLASSES: string[];
3
3
  export declare const BAR_CLASSES: string[];
4
4
  export declare const FOLD_CLASSES: string[];
5
5
  export declare const FOLD_VARIABLES: string[];
6
- export declare function foldCssFor(hingeBounds: SegmentRect | undefined, hingeOrientation: 'horizontal' | 'vertical' | undefined): {
6
+ export declare function foldCssFor(hingeBounds: SegmentRect | undefined, hingeOrientation: 'horizontal' | 'vertical' | undefined, hingeMargins?: {
7
+ top: number;
8
+ right: number;
9
+ bottom: number;
10
+ left: number;
11
+ }): {
7
12
  variables: Record<string, string>;
8
13
  className: string | null;
9
14
  };
package/dist/esm/css.js CHANGED
@@ -6,16 +6,30 @@ export const CSS_CLASSES = [
6
6
  ];
7
7
  export const BAR_CLASSES = ['vertical-bars-leading', 'vertical-bars-trailing'];
8
8
  export const FOLD_CLASSES = ['fold-vertical', 'fold-horizontal'];
9
- export const FOLD_VARIABLES = ['--fold-left', '--fold-top', '--fold-width', '--fold-height'];
10
- export function foldCssFor(hingeBounds, hingeOrientation) {
9
+ export const FOLD_VARIABLES = [
10
+ '--fold-left',
11
+ '--fold-top',
12
+ '--fold-width',
13
+ '--fold-height',
14
+ '--fold-margin-top',
15
+ '--fold-margin-right',
16
+ '--fold-margin-bottom',
17
+ '--fold-margin-left',
18
+ ];
19
+ export function foldCssFor(hingeBounds, hingeOrientation, hingeMargins) {
11
20
  if (!hingeBounds || !hingeOrientation)
12
21
  return { variables: {}, className: null };
22
+ const margins = hingeMargins !== null && hingeMargins !== void 0 ? hingeMargins : { top: 0, right: 0, bottom: 0, left: 0 };
13
23
  return {
14
24
  variables: {
15
25
  '--fold-left': `${hingeBounds.x}px`,
16
26
  '--fold-top': `${hingeBounds.y}px`,
17
27
  '--fold-width': `${hingeBounds.width}px`,
18
28
  '--fold-height': `${hingeBounds.height}px`,
29
+ '--fold-margin-top': `${margins.top}px`,
30
+ '--fold-margin-right': `${margins.right}px`,
31
+ '--fold-margin-bottom': `${margins.bottom}px`,
32
+ '--fold-margin-left': `${margins.left}px`,
19
33
  },
20
34
  className: `fold-${hingeOrientation}`,
21
35
  };
@@ -1 +1 @@
1
- {"version":3,"file":"css.js","sourceRoot":"","sources":["../../src/css.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SegmentRect } from './segments';\n\nexport const CSS_CLASSES = [\n 'horizontal-viewport-segments-2',\n 'vertical-viewport-segments-2',\n 'device-posture-continuous',\n 'device-posture-folded',\n];\n\nexport const BAR_CLASSES = ['vertical-bars-leading', 'vertical-bars-trailing'];\n\nexport const FOLD_CLASSES = ['fold-vertical', 'fold-horizontal'];\n\nexport const FOLD_VARIABLES = ['--fold-left', '--fold-top', '--fold-width', '--fold-height'];\n\nexport function foldCssFor(\n hingeBounds: SegmentRect | undefined,\n hingeOrientation: 'horizontal' | 'vertical' | undefined,\n): { variables: Record<string, string>; className: string | null } {\n if (!hingeBounds || !hingeOrientation) return { variables: {}, className: null };\n\n return {\n variables: {\n '--fold-left': `${hingeBounds.x}px`,\n '--fold-top': `${hingeBounds.y}px`,\n '--fold-width': `${hingeBounds.width}px`,\n '--fold-height': `${hingeBounds.height}px`,\n },\n className: `fold-${hingeOrientation}`,\n };\n}\n\nexport function barClassFor(edge: 'leading' | 'trailing' | null): string | null {\n return edge ? `vertical-bars-${edge}` : null;\n}\n\nconst TAB_BAR_MARGIN = 24;\nconst CAMERA_GAP = 15;\n\nexport function verticalTabBarBottom(cameras: SegmentRect[], viewportHeight: number): number {\n const below = cameras.filter((camera) => camera.y > viewportHeight / 2);\n const camera = below.sort((a, b) => a.width * a.height - b.width * b.height)[0];\n if (!camera) return TAB_BAR_MARGIN;\n return Math.max(TAB_BAR_MARGIN, Math.round(viewportHeight - camera.y + CAMERA_GAP));\n}\n\nexport function cssFor(\n segments: SegmentRect[],\n posture: 'continuous' | 'folded',\n): { variables: Record<string, string>; classes: string[] } {\n const stacked = segments.length > 1 && segments[1].y > segments[0].y;\n const variables: Record<string, string> = {};\n\n segments.forEach((segment, index) => {\n const suffix = stacked ? `0-${index}` : `${index}-0`;\n variables[`--viewport-segment-top-${suffix}`] = `${segment.y}px`;\n variables[`--viewport-segment-left-${suffix}`] = `${segment.x}px`;\n variables[`--viewport-segment-bottom-${suffix}`] = `${segment.y + segment.height}px`;\n variables[`--viewport-segment-right-${suffix}`] = `${segment.x + segment.width}px`;\n variables[`--viewport-segment-width-${suffix}`] = `${segment.width}px`;\n variables[`--viewport-segment-height-${suffix}`] = `${segment.height}px`;\n });\n\n const classes = [`device-posture-${posture}`];\n if (segments.length > 1) {\n classes.push(stacked ? 'vertical-viewport-segments-2' : 'horizontal-viewport-segments-2');\n }\n\n return { variables, classes };\n}\n"]}
1
+ {"version":3,"file":"css.js","sourceRoot":"","sources":["../../src/css.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SegmentRect } from './segments';\n\nexport const CSS_CLASSES = [\n 'horizontal-viewport-segments-2',\n 'vertical-viewport-segments-2',\n 'device-posture-continuous',\n 'device-posture-folded',\n];\n\nexport const BAR_CLASSES = ['vertical-bars-leading', 'vertical-bars-trailing'];\n\nexport const FOLD_CLASSES = ['fold-vertical', 'fold-horizontal'];\n\nexport const FOLD_VARIABLES = [\n '--fold-left',\n '--fold-top',\n '--fold-width',\n '--fold-height',\n '--fold-margin-top',\n '--fold-margin-right',\n '--fold-margin-bottom',\n '--fold-margin-left',\n];\n\nexport function foldCssFor(\n hingeBounds: SegmentRect | undefined,\n hingeOrientation: 'horizontal' | 'vertical' | undefined,\n hingeMargins?: { top: number; right: number; bottom: number; left: number },\n): { variables: Record<string, string>; className: string | null } {\n if (!hingeBounds || !hingeOrientation) return { variables: {}, className: null };\n\n const margins = hingeMargins ?? { top: 0, right: 0, bottom: 0, left: 0 };\n\n return {\n variables: {\n '--fold-left': `${hingeBounds.x}px`,\n '--fold-top': `${hingeBounds.y}px`,\n '--fold-width': `${hingeBounds.width}px`,\n '--fold-height': `${hingeBounds.height}px`,\n '--fold-margin-top': `${margins.top}px`,\n '--fold-margin-right': `${margins.right}px`,\n '--fold-margin-bottom': `${margins.bottom}px`,\n '--fold-margin-left': `${margins.left}px`,\n },\n className: `fold-${hingeOrientation}`,\n };\n}\n\nexport function barClassFor(edge: 'leading' | 'trailing' | null): string | null {\n return edge ? `vertical-bars-${edge}` : null;\n}\n\nconst TAB_BAR_MARGIN = 24;\nconst CAMERA_GAP = 15;\n\nexport function verticalTabBarBottom(cameras: SegmentRect[], viewportHeight: number): number {\n const below = cameras.filter((camera) => camera.y > viewportHeight / 2);\n const camera = below.sort((a, b) => a.width * a.height - b.width * b.height)[0];\n if (!camera) return TAB_BAR_MARGIN;\n return Math.max(TAB_BAR_MARGIN, Math.round(viewportHeight - camera.y + CAMERA_GAP));\n}\n\nexport function cssFor(\n segments: SegmentRect[],\n posture: 'continuous' | 'folded',\n): { variables: Record<string, string>; classes: string[] } {\n const stacked = segments.length > 1 && segments[1].y > segments[0].y;\n const variables: Record<string, string> = {};\n\n segments.forEach((segment, index) => {\n const suffix = stacked ? `0-${index}` : `${index}-0`;\n variables[`--viewport-segment-top-${suffix}`] = `${segment.y}px`;\n variables[`--viewport-segment-left-${suffix}`] = `${segment.x}px`;\n variables[`--viewport-segment-bottom-${suffix}`] = `${segment.y + segment.height}px`;\n variables[`--viewport-segment-right-${suffix}`] = `${segment.x + segment.width}px`;\n variables[`--viewport-segment-width-${suffix}`] = `${segment.width}px`;\n variables[`--viewport-segment-height-${suffix}`] = `${segment.height}px`;\n });\n\n const classes = [`device-posture-${posture}`];\n if (segments.length > 1) {\n classes.push(stacked ? 'vertical-viewport-segments-2' : 'horizontal-viewport-segments-2');\n }\n\n return { variables, classes };\n}\n"]}