@erkamyaman/capacitor-foldable 8.0.4 → 8.2.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.
package/README.md CHANGED
@@ -96,6 +96,19 @@ window.viewport.segments; // two DOMRects when the fold splits the web view
96
96
  | `@media (vertical-viewport-segments: 2)` | `.vertical-viewport-segments-2` |
97
97
  | `env(viewport-segment-width 0 0)` | `var(--viewport-segment-width-0-0)` |
98
98
  | Native bars moved to the side (iPhone Duo) | `.vertical-bars-leading`, `.vertical-bars-trailing` |
99
+ | The phone is being folded right now | `.folding` |
100
+
101
+ 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:
102
+
103
+ | Fold | With the polyfill |
104
+ | --- | --- |
105
+ | Direction | `.fold-vertical`, `.fold-horizontal` |
106
+ | Position and size | `var(--fold-left)`, `var(--fold-top)`, `var(--fold-width)`, `var(--fold-height)` |
107
+
108
+ ```css
109
+ .fold-vertical .game { width: var(--fold-left); }
110
+ .fold-vertical .controls { left: calc(var(--fold-left) + var(--fold-width)); }
111
+ ```
99
112
 
100
113
  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).
101
114
 
@@ -104,6 +117,12 @@ Using Ionic's `ion-tabs`? `import '@erkamyaman/capacitor-foldable/ionic-tabs.css
104
117
  | <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"> |
105
118
  | <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"> |
106
119
 
120
+ ## Apps built with it
121
+
122
+ - **[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.
123
+ - **[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.
124
+ - **[example-app](example-app)** is the plain single-screen app that shows every value the plugin reports at once.
125
+
107
126
  ## Guides
108
127
 
109
128
  - [Examples](docs/examples.md)
@@ -118,6 +137,8 @@ Using Ionic's `ion-tabs`? `import '@erkamyaman/capacitor-foldable/ionic-tabs.css
118
137
  * [`isDeviceFoldable()`](#isdevicefoldable)
119
138
  * [`getFoldState()`](#getfoldstate)
120
139
  * [`getHingeAngle()`](#gethingeangle)
140
+ * [`getReservedRegions()`](#getreservedregions)
141
+ * [`setVerticalBarBehavior(...)`](#setverticalbarbehavior)
121
142
  * [`getSizeClass()`](#getsizeclass)
122
143
  * [`getDisplayModes()`](#getdisplaymodes)
123
144
  * [`getBarPlacement()`](#getbarplacement)
@@ -129,6 +150,7 @@ Using Ionic's `ion-tabs`? `import '@erkamyaman/capacitor-foldable/ionic-tabs.css
129
150
  * [`addListener('hingeAngleChange', ...)`](#addlistenerhingeanglechange-)
130
151
  * [`addListener('sizeClassChange', ...)`](#addlistenersizeclasschange-)
131
152
  * [`addListener('displayModeChange', ...)`](#addlistenerdisplaymodechange-)
153
+ * [`addListener('foldingChange', ...)`](#addlistenerfoldingchange-)
132
154
  * [`addListener('barPlacementChange', ...)`](#addlistenerbarplacementchange-)
133
155
  * [Interfaces](#interfaces)
134
156
  * [Type Aliases](#type-aliases)
@@ -175,20 +197,61 @@ fold information.
175
197
  ### getHingeAngle()
176
198
 
177
199
  ```typescript
178
- getHingeAngle() => Promise<{ angle: number | null; }>
200
+ getHingeAngle() => Promise<{ angle: number | null; status: HingeStatus | null; }>
179
201
  ```
180
202
 
181
203
  Read the angle between the two halves of the device, in degrees: `0` when
182
204
  closed, `180` when flat. Resolves to `{ angle: null }` on devices without a
183
205
  hinge angle sensor, on web, and on iOS before 27.1.
184
206
 
185
- **Returns:** <code>Promise&lt;{ angle: number | null; }&gt;</code>
207
+ **Returns:** <code>Promise&lt;{ angle: number | null; status: <a href="#hingestatus">HingeStatus</a> | null; }&gt;</code>
186
208
 
187
209
  **Since:** 7.0.0
188
210
 
189
211
  --------------------
190
212
 
191
213
 
214
+ ### getReservedRegions()
215
+
216
+ ```typescript
217
+ getReservedRegions() => Promise<{ regions: ReservedRegion[]; }>
218
+ ```
219
+
220
+ Read every region the system reserves on this display: the fold and
221
+ anything covering the screen, active or not. iPhone Duo reports the fold,
222
+ the vertical status bar area and the under-display camera. Resolves to an
223
+ empty list on Android, on web and on iOS before 27.1.
224
+
225
+ **Returns:** <code>Promise&lt;{ regions: ReservedRegion[]; }&gt;</code>
226
+
227
+ **Since:** 8.2.0
228
+
229
+ --------------------
230
+
231
+
232
+ ### setVerticalBarBehavior(...)
233
+
234
+ ```typescript
235
+ setVerticalBarBehavior(options: { behavior: 'automatic' | 'disabled'; }) => Promise<{ applied: boolean; }>
236
+ ```
237
+
238
+ Choose whether iPhone Duo may move this app's bars to the side of the
239
+ display. `'disabled'` keeps everything horizontal, including the status
240
+ bar. Resolves to `{ applied: false }` when the app does not use
241
+ `FoldableBridgeViewController`, on Android, on web and on iOS before 27.1.
242
+ See the iPhone Duo guide for the one-line storyboard change.
243
+
244
+ | Param | Type |
245
+ | ------------- | ----------------------------------------------------- |
246
+ | **`options`** | <code>{ behavior: 'automatic' \| 'disabled'; }</code> |
247
+
248
+ **Returns:** <code>Promise&lt;{ applied: boolean; }&gt;</code>
249
+
250
+ **Since:** 8.2.0
251
+
252
+ --------------------
253
+
254
+
192
255
  ### getSizeClass()
193
256
 
194
257
  ```typescript
@@ -391,6 +454,30 @@ the user folded or unfolded the device. Never fires on iOS and web.
391
454
  --------------------
392
455
 
393
456
 
457
+ ### addListener('foldingChange', ...)
458
+
459
+ ```typescript
460
+ addListener(eventName: 'foldingChange', listenerFunc: (event: { folding: boolean; }) => void) => Promise<PluginListenerHandle>
461
+ ```
462
+
463
+ Listen for the device being folded or unfolded. `folding` turns `true` as
464
+ soon as the hinge starts moving and `false` about half a second after it
465
+ stops, so an app can pause animations or heavy work while the screen is in
466
+ motion. Only fires where a hinge angle is available: Android foldables with
467
+ a hinge sensor, and iPhone Duo on iOS 27.1 or later.
468
+
469
+ | Param | Type |
470
+ | ------------------ | ------------------------------------------------------ |
471
+ | **`eventName`** | <code>'foldingChange'</code> |
472
+ | **`listenerFunc`** | <code>(event: { folding: boolean; }) =&gt; void</code> |
473
+
474
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
475
+
476
+ **Since:** 8.2.0
477
+
478
+ --------------------
479
+
480
+
394
481
  ### addListener('barPlacementChange', ...)
395
482
 
396
483
  ```typescript
@@ -417,16 +504,30 @@ Only fires on iOS 27.1 or later.
417
504
 
418
505
  #### FoldState
419
506
 
420
- | Prop | Type | Description | Since |
421
- | ---------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
422
- | **`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 |
423
- | **`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 |
424
- | **`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 |
425
- | **`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 |
426
- | **`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 |
427
- | **`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 |
428
- | **`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 |
429
- | **`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 |
507
+ | Prop | Type | Description | Since |
508
+ | ---------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
509
+ | **`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 |
510
+ | **`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 |
511
+ | **`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 |
512
+ | **`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 |
513
+ | **`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 |
514
+ | **`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 |
515
+ | **`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 |
516
+ | **`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 |
517
+ | **`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 |
518
+
519
+
520
+ #### ReservedRegion
521
+
522
+ | Prop | Type | Description | Since |
523
+ | -------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----- |
524
+ | **`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 |
525
+ | **`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 |
526
+ | **`x`** | <code>number</code> | | |
527
+ | **`y`** | <code>number</code> | | |
528
+ | **`width`** | <code>number</code> | | |
529
+ | **`height`** | <code>number</code> | | |
530
+ | **`margins`** | <code>{ top: number; right: number; bottom: number; left: number; }</code> | Space to keep clear around the region, in CSS pixels. | 8.2.0 |
430
531
 
431
532
 
432
533
  #### SizeClass
@@ -464,6 +565,14 @@ Only fires on iOS 27.1 or later.
464
565
  ### Type Aliases
465
566
 
466
567
 
568
+ #### HingeStatus
569
+
570
+ How far open the hinge is, as the system sees it. Note that it can lag the
571
+ angle: iOS keeps reporting `'closed'` for a moment after the phone opens.
572
+
573
+ <code>'closed' | 'partiallyOpen' | 'fullyOpen'</code>
574
+
575
+
467
576
  #### DisplayModeStatus
468
577
 
469
578
  <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
@@ -1,6 +1,17 @@
1
1
  import type { SegmentRect } from './segments';
2
2
  export declare const CSS_CLASSES: string[];
3
3
  export declare const BAR_CLASSES: string[];
4
+ export declare const FOLD_CLASSES: string[];
5
+ export declare const FOLD_VARIABLES: string[];
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
+ }): {
12
+ variables: Record<string, string>;
13
+ className: string | null;
14
+ };
4
15
  export declare function barClassFor(edge: 'leading' | 'trailing' | null): string | null;
5
16
  export declare function verticalTabBarBottom(cameras: SegmentRect[], viewportHeight: number): number;
6
17
  export declare function cssFor(segments: SegmentRect[], posture: 'continuous' | 'folded'): {
package/dist/esm/css.js CHANGED
@@ -5,6 +5,35 @@ export const CSS_CLASSES = [
5
5
  'device-posture-folded',
6
6
  ];
7
7
  export const BAR_CLASSES = ['vertical-bars-leading', 'vertical-bars-trailing'];
8
+ export const FOLD_CLASSES = ['fold-vertical', 'fold-horizontal'];
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) {
20
+ if (!hingeBounds || !hingeOrientation)
21
+ return { variables: {}, className: null };
22
+ const margins = hingeMargins !== null && hingeMargins !== void 0 ? hingeMargins : { top: 0, right: 0, bottom: 0, left: 0 };
23
+ return {
24
+ variables: {
25
+ '--fold-left': `${hingeBounds.x}px`,
26
+ '--fold-top': `${hingeBounds.y}px`,
27
+ '--fold-width': `${hingeBounds.width}px`,
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`,
33
+ },
34
+ className: `fold-${hingeOrientation}`,
35
+ };
36
+ }
8
37
  export function barClassFor(edge) {
9
38
  return edge ? `vertical-bars-${edge}` : null;
10
39
  }
@@ -1 +1 @@
1
- {"version":3,"file":"css.js","sourceRoot":"","sources":["../../src/css.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,gCAAgC;IAChC,8BAA8B;IAC9B,2BAA2B;IAC3B,uBAAuB;CACxB,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,uBAAuB,EAAE,wBAAwB,CAAC,CAAC;AAE/E,MAAM,UAAU,WAAW,CAAC,IAAmC;IAC7D,OAAO,IAAI,CAAC,CAAC,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/C,CAAC;AAED,MAAM,cAAc,GAAG,EAAE,CAAC;AAC1B,MAAM,UAAU,GAAG,EAAE,CAAC;AAEtB,MAAM,UAAU,oBAAoB,CAAC,OAAsB,EAAE,cAAsB;IACjF,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,GAAG,cAAc,GAAG,CAAC,CAAC,CAAC;IACxE,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAChF,IAAI,CAAC,MAAM;QAAE,OAAO,cAAc,CAAC;IACnC,OAAO,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,GAAG,MAAM,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC;AACtF,CAAC;AAED,MAAM,UAAU,MAAM,CACpB,QAAuB,EACvB,OAAgC;IAEhC,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACrE,MAAM,SAAS,GAA2B,EAAE,CAAC;IAE7C,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE;QAClC,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,CAAC;QACrD,SAAS,CAAC,0BAA0B,MAAM,EAAE,CAAC,GAAG,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC;QACjE,SAAS,CAAC,2BAA2B,MAAM,EAAE,CAAC,GAAG,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC;QAClE,SAAS,CAAC,6BAA6B,MAAM,EAAE,CAAC,GAAG,GAAG,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC;QACrF,SAAS,CAAC,4BAA4B,MAAM,EAAE,CAAC,GAAG,GAAG,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,IAAI,CAAC;QACnF,SAAS,CAAC,4BAA4B,MAAM,EAAE,CAAC,GAAG,GAAG,OAAO,CAAC,KAAK,IAAI,CAAC;QACvE,SAAS,CAAC,6BAA6B,MAAM,EAAE,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC;IAC3E,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,CAAC,kBAAkB,OAAO,EAAE,CAAC,CAAC;IAC9C,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,8BAA8B,CAAC,CAAC,CAAC,gCAAgC,CAAC,CAAC;IAC5F,CAAC;IAED,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;AAChC,CAAC","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 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"]}