expo-dev-menu 58.0.3 → 58.0.5
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/CHANGELOG.md +10 -0
- package/android/build.gradle +2 -2
- package/expo-dev-menu.podspec +6 -0
- package/ios/FAB/DevMenuFABView.swift +67 -21
- package/ios/FAB/FABPlacement.swift +39 -0
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 58.0.5 — 2026-09-22
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug fixes
|
|
16
|
+
|
|
17
|
+
- [iOS] Kept the floating dev menu button clear of system bars, the front camera and the home indicator by clamping the drawn button, rather than its touch frame, to the safe area on every edge. ([#50445](https://github.com/expo/expo/pull/50445) by [@alanjhughes](https://github.com/alanjhughes))
|
|
18
|
+
|
|
19
|
+
## 58.0.4 — 2026-09-21
|
|
20
|
+
|
|
21
|
+
_This version does not introduce any user-facing changes._
|
|
22
|
+
|
|
13
23
|
## 58.0.3 — 2026-09-16
|
|
14
24
|
|
|
15
25
|
_This version does not introduce any user-facing changes._
|
package/android/build.gradle
CHANGED
|
@@ -12,7 +12,7 @@ apply plugin: 'expo-module-gradle-plugin'
|
|
|
12
12
|
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
|
|
13
13
|
|
|
14
14
|
group = 'host.exp.exponent'
|
|
15
|
-
version = '58.0.
|
|
15
|
+
version = '58.0.5'
|
|
16
16
|
|
|
17
17
|
def hasDevLauncher = findProject(":expo-dev-launcher") != null
|
|
18
18
|
def configureInRelease = findProperty("expo.devmenu.configureInRelease") == "true"
|
|
@@ -29,7 +29,7 @@ android {
|
|
|
29
29
|
|
|
30
30
|
defaultConfig {
|
|
31
31
|
versionCode 10
|
|
32
|
-
versionName '58.0.
|
|
32
|
+
versionName '58.0.5'
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
buildTypes {
|
package/expo-dev-menu.podspec
CHANGED
|
@@ -115,6 +115,9 @@ Pod::Spec.new do |s|
|
|
|
115
115
|
test_spec.platforms = {
|
|
116
116
|
:ios => '16.4'
|
|
117
117
|
}
|
|
118
|
+
test_spec.pod_target_xcconfig = {
|
|
119
|
+
'OTHER_LDFLAGS' => '$(inherited) -lc++'
|
|
120
|
+
}
|
|
118
121
|
end
|
|
119
122
|
|
|
120
123
|
s.test_spec 'UITests' do |test_spec|
|
|
@@ -128,6 +131,9 @@ Pod::Spec.new do |s|
|
|
|
128
131
|
test_spec.platforms = {
|
|
129
132
|
:ios => '16.4'
|
|
130
133
|
}
|
|
134
|
+
test_spec.pod_target_xcconfig = {
|
|
135
|
+
'OTHER_LDFLAGS' => '$(inherited) -lc++'
|
|
136
|
+
}
|
|
131
137
|
end
|
|
132
138
|
|
|
133
139
|
s.default_subspec = ['Main']
|
|
@@ -22,6 +22,14 @@ enum FABConstants {
|
|
|
22
22
|
)
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
struct FABPillHeightKey: PreferenceKey {
|
|
26
|
+
static let defaultValue: CGFloat = FABConstants.iconSize
|
|
27
|
+
|
|
28
|
+
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
|
29
|
+
value = nextValue()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
struct FabPill: View {
|
|
26
34
|
@Binding var isPressed: Bool
|
|
27
35
|
@Binding var isDragging: Bool
|
|
@@ -63,6 +71,11 @@ struct FabPill: View {
|
|
|
63
71
|
.transition(.opacity.combined(with: .scale(scale: 0.8)))
|
|
64
72
|
}
|
|
65
73
|
}
|
|
74
|
+
.background(
|
|
75
|
+
GeometryReader { proxy in
|
|
76
|
+
Color.clear.preference(key: FABPillHeightKey.self, value: proxy.size.height)
|
|
77
|
+
}
|
|
78
|
+
)
|
|
66
79
|
.saturation(isIdle ? 0 : 1)
|
|
67
80
|
.opacity(isIdle ? 0.5 : 1)
|
|
68
81
|
.animation(.easeInOut(duration: 0.3), value: isIdle)
|
|
@@ -113,6 +126,18 @@ struct DevMenuFABView: View {
|
|
|
113
126
|
|
|
114
127
|
private let fabSize = CGSize(width: 72, height: FABConstants.iconSize + 50)
|
|
115
128
|
|
|
129
|
+
/// Centred in the touch frame, so its top moves as the label appears and dismisses.
|
|
130
|
+
private var drawnFrame: CGRect {
|
|
131
|
+
let ring = FABPlacement.ringOverhang
|
|
132
|
+
let width = FABConstants.iconSize + ring * 2
|
|
133
|
+
return CGRect(
|
|
134
|
+
x: (fabSize.width - width) / 2,
|
|
135
|
+
y: (fabSize.height - pillHeight) / 2 - ring,
|
|
136
|
+
width: width,
|
|
137
|
+
height: pillHeight + ring * 2
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
116
141
|
// UserDefaults keys for persisting position
|
|
117
142
|
private static let positionXKey = "DevMenuFAB.positionX"
|
|
118
143
|
private static let positionYKey = "DevMenuFAB.positionY"
|
|
@@ -136,6 +161,7 @@ struct DevMenuFABView: View {
|
|
|
136
161
|
@State private var isPressed = false
|
|
137
162
|
@State private var dragStartPosition: CGPoint = .zero
|
|
138
163
|
@State private var didPosition = false
|
|
164
|
+
@State private var pillHeight: CGFloat = FABConstants.iconSize
|
|
139
165
|
|
|
140
166
|
// Get safe area from window since .ignoresSafeArea() may zero out geometry values
|
|
141
167
|
private var windowSafeArea: UIEdgeInsets {
|
|
@@ -164,6 +190,21 @@ struct DevMenuFABView: View {
|
|
|
164
190
|
|
|
165
191
|
FabPill(isPressed: $isPressed, isDragging: $isDragging)
|
|
166
192
|
.frame(width: fabSize.width, height: fabSize.height)
|
|
193
|
+
.onPreferenceChange(FABPillHeightKey.self) { height in
|
|
194
|
+
pillHeight = height
|
|
195
|
+
let ranges = placementRanges(bounds: geometry.size, safeArea: safeArea)
|
|
196
|
+
let clamped = CGPoint(
|
|
197
|
+
x: position.x.clamped(to: ranges.x),
|
|
198
|
+
y: position.y.clamped(to: ranges.y)
|
|
199
|
+
)
|
|
200
|
+
guard clamped != position else { return }
|
|
201
|
+
var transaction = Transaction()
|
|
202
|
+
transaction.disablesAnimations = true
|
|
203
|
+
withTransaction(transaction) {
|
|
204
|
+
position = clamped
|
|
205
|
+
}
|
|
206
|
+
onFrameChange(CGRect(origin: clamped, size: fabSize))
|
|
207
|
+
}
|
|
167
208
|
.position(x: currentFrame.midX, y: currentFrame.midY)
|
|
168
209
|
.gesture(dragGesture(bounds: geometry.size, safeArea: safeArea))
|
|
169
210
|
.onAppear {
|
|
@@ -236,15 +277,10 @@ struct DevMenuFABView: View {
|
|
|
236
277
|
private func placeInitially(bounds: CGSize, safeArea: EdgeInsets) {
|
|
237
278
|
let initialPos: CGPoint
|
|
238
279
|
if let storedPos = Self.loadStoredPosition() {
|
|
239
|
-
let
|
|
240
|
-
let minX = margin / 2
|
|
241
|
-
let maxX = bounds.width - fabSize.width - margin / 2
|
|
242
|
-
let minY = safeArea.top + FABConstants.verticalPadding
|
|
243
|
-
let maxY = bounds.height - fabSize.height - safeArea.bottom - FABConstants.verticalPadding
|
|
244
|
-
|
|
280
|
+
let ranges = placementRanges(bounds: bounds, safeArea: safeArea)
|
|
245
281
|
initialPos = CGPoint(
|
|
246
|
-
x: storedPos.x.clamped(to:
|
|
247
|
-
y: storedPos.y.clamped(to:
|
|
282
|
+
x: storedPos.x.clamped(to: ranges.x),
|
|
283
|
+
y: storedPos.y.clamped(to: ranges.y)
|
|
248
284
|
)
|
|
249
285
|
} else {
|
|
250
286
|
initialPos = defaultPosition(bounds: bounds, safeArea: safeArea)
|
|
@@ -258,11 +294,27 @@ struct DevMenuFABView: View {
|
|
|
258
294
|
}
|
|
259
295
|
}
|
|
260
296
|
|
|
261
|
-
private func
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
297
|
+
private func placementRanges(
|
|
298
|
+
bounds: CGSize,
|
|
299
|
+
safeArea: EdgeInsets
|
|
300
|
+
) -> (x: ClosedRange<CGFloat>, y: ClosedRange<CGFloat>) {
|
|
301
|
+
let ranges = FABPlacement.ranges(
|
|
302
|
+
bounds: bounds,
|
|
303
|
+
safeArea: FABInsets(
|
|
304
|
+
top: safeArea.top,
|
|
305
|
+
leading: safeArea.leading,
|
|
306
|
+
bottom: safeArea.bottom,
|
|
307
|
+
trailing: safeArea.trailing
|
|
308
|
+
),
|
|
309
|
+
drawnFrame: drawnFrame,
|
|
310
|
+
inset: FABConstants.margin
|
|
265
311
|
)
|
|
312
|
+
return (ranges.x, ranges.y)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private func defaultPosition(bounds: CGSize, safeArea: EdgeInsets) -> CGPoint {
|
|
316
|
+
let ranges = placementRanges(bounds: bounds, safeArea: safeArea)
|
|
317
|
+
return CGPoint(x: ranges.x.upperBound, y: ranges.y.lowerBound)
|
|
266
318
|
}
|
|
267
319
|
|
|
268
320
|
private func snapToEdge(
|
|
@@ -271,19 +323,13 @@ struct DevMenuFABView: View {
|
|
|
271
323
|
bounds: CGSize,
|
|
272
324
|
safeArea: EdgeInsets
|
|
273
325
|
) -> CGPoint {
|
|
274
|
-
let margin = FABConstants.margin
|
|
275
|
-
let edgeMargin = margin / 2 // Closer to screen edge when snapped
|
|
276
326
|
let momentumX = velocity.x * FABConstants.momentumFactor
|
|
277
327
|
let momentumY = velocity.y * FABConstants.momentumFactor
|
|
328
|
+
let ranges = placementRanges(bounds: bounds, safeArea: safeArea)
|
|
278
329
|
|
|
279
330
|
let estimatedCenterX = point.x + self.fabSize.width / 2 + momentumX
|
|
280
|
-
let targetX
|
|
281
|
-
|
|
282
|
-
: bounds.width - self.fabSize.width - edgeMargin
|
|
283
|
-
|
|
284
|
-
let minY = safeArea.top + FABConstants.verticalPadding
|
|
285
|
-
let maxY = bounds.height - self.fabSize.height - safeArea.bottom - FABConstants.verticalPadding
|
|
286
|
-
let targetY = (point.y + momentumY).clamped(to: minY...maxY)
|
|
331
|
+
let targetX = estimatedCenterX < bounds.width / 2 ? ranges.x.lowerBound : ranges.x.upperBound
|
|
332
|
+
let targetY = (point.y + momentumY).clamped(to: ranges.y)
|
|
287
333
|
|
|
288
334
|
return CGPoint(x: targetX, y: targetY)
|
|
289
335
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import CoreGraphics
|
|
4
|
+
|
|
5
|
+
struct FABInsets {
|
|
6
|
+
let top: CGFloat
|
|
7
|
+
let leading: CGFloat
|
|
8
|
+
let bottom: CGFloat
|
|
9
|
+
let trailing: CGFloat
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
struct FABPlacementRanges {
|
|
13
|
+
let x: ClosedRange<CGFloat>
|
|
14
|
+
let y: ClosedRange<CGFloat>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
enum FABPlacement {
|
|
18
|
+
/// The ring is stroked on a path 4pt wider than the icon, so it paints this far outside it.
|
|
19
|
+
static let ringOverhang: CGFloat = 4
|
|
20
|
+
|
|
21
|
+
/// Origins the FAB frame may take, given the painted pill in that frame's coordinate space.
|
|
22
|
+
static func ranges(
|
|
23
|
+
bounds: CGSize,
|
|
24
|
+
safeArea: FABInsets,
|
|
25
|
+
drawnFrame: CGRect,
|
|
26
|
+
inset: CGFloat
|
|
27
|
+
) -> FABPlacementRanges {
|
|
28
|
+
// Keep `inset` from each edge, but never sit further in than the safe area already requires.
|
|
29
|
+
let minX = max(safeArea.leading, inset) - drawnFrame.minX
|
|
30
|
+
let maxX = bounds.width - max(safeArea.trailing, inset) - drawnFrame.maxX
|
|
31
|
+
let minY = max(safeArea.top, inset) - drawnFrame.minY
|
|
32
|
+
let maxY = bounds.height - max(safeArea.bottom, inset) - drawnFrame.maxY
|
|
33
|
+
|
|
34
|
+
return FABPlacementRanges(
|
|
35
|
+
x: min(minX, maxX)...max(minX, maxX),
|
|
36
|
+
y: min(minY, maxY)...max(minY, maxY)
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-dev-menu",
|
|
3
|
-
"version": "58.0.
|
|
3
|
+
"version": "58.0.5",
|
|
4
4
|
"description": "Expo/React Native module with the developer menu.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dev",
|
|
@@ -31,16 +31,16 @@
|
|
|
31
31
|
"@testing-library/react-native": "^13.3.0",
|
|
32
32
|
"@types/jest": "^29.2.1",
|
|
33
33
|
"@types/node": "^22.14.0",
|
|
34
|
-
"babel-preset-expo": "58.0.
|
|
34
|
+
"babel-preset-expo": "58.0.4",
|
|
35
35
|
"expo-module-scripts": "56.0.3",
|
|
36
|
-
"react": "19.
|
|
37
|
-
"react-native": "0.88.0-rc.
|
|
36
|
+
"react": "19.3.0",
|
|
37
|
+
"react-native": "0.88.0-rc.1"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"expo": "*",
|
|
41
41
|
"react-native": "*"
|
|
42
42
|
},
|
|
43
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "add8cbf49f477cfd2283ae05af35b4713a773c55",
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "pnpm run build:lib && pnpm run build:plugin",
|
|
46
46
|
"build:lib": "expo-build src",
|