@buoy-gg/core 2.1.14 → 2.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/lib/commonjs/floatingMenu/DevToolsSettingsModal.js +3 -33
- package/lib/commonjs/floatingMenu/DevToolsSettingsModal.web.js +3 -25
- package/lib/commonjs/floatingMenu/FloatingDevTools.js +14 -1
- package/lib/commonjs/floatingMenu/FloatingDevTools.web.js +19 -9
- package/lib/commonjs/floatingMenu/FloatingMenu.js +6 -2
- package/lib/commonjs/floatingMenu/dial/DialDevTools.js +166 -196
- package/lib/commonjs/floatingMenu/dial/DialDevTools.web.js +82 -7
- package/lib/commonjs/floatingMenu/dial/DialIcon.js +66 -59
- package/lib/commonjs/floatingMenu/dial/DialPagination.js +170 -0
- package/lib/commonjs/floatingMenu/dial/dialUsageStore.js +97 -0
- package/lib/module/floatingMenu/DevToolsSettingsModal.js +3 -33
- package/lib/module/floatingMenu/DevToolsSettingsModal.web.js +4 -28
- package/lib/module/floatingMenu/FloatingDevTools.js +14 -1
- package/lib/module/floatingMenu/FloatingDevTools.web.js +19 -9
- package/lib/module/floatingMenu/FloatingMenu.js +6 -2
- package/lib/module/floatingMenu/dial/DialDevTools.js +166 -196
- package/lib/module/floatingMenu/dial/DialDevTools.web.js +82 -7
- package/lib/module/floatingMenu/dial/DialIcon.js +67 -60
- package/lib/module/floatingMenu/dial/DialPagination.js +165 -0
- package/lib/module/floatingMenu/dial/dialUsageStore.js +89 -0
- package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/FloatingMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts +0 -2
- package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/dial/DialIcon.d.ts +7 -2
- package/lib/typescript/commonjs/floatingMenu/dial/DialIcon.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/dial/DialPagination.d.ts +22 -0
- package/lib/typescript/commonjs/floatingMenu/dial/DialPagination.d.ts.map +1 -0
- package/lib/typescript/commonjs/floatingMenu/dial/dialUsageStore.d.ts +34 -0
- package/lib/typescript/commonjs/floatingMenu/dial/dialUsageStore.d.ts.map +1 -0
- package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/FloatingMenu.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts +0 -2
- package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/dial/DialIcon.d.ts +7 -2
- package/lib/typescript/module/floatingMenu/dial/DialIcon.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/dial/DialPagination.d.ts +22 -0
- package/lib/typescript/module/floatingMenu/dial/DialPagination.d.ts.map +1 -0
- package/lib/typescript/module/floatingMenu/dial/dialUsageStore.d.ts +34 -0
- package/lib/typescript/module/floatingMenu/dial/dialUsageStore.d.ts.map +1 -0
- package/package.json +5 -5
|
@@ -227,8 +227,12 @@ export const FloatingMenu = ({
|
|
|
227
227
|
message: "Grab and position this menu wherever you want, then tap the icon to open your tools."
|
|
228
228
|
})]
|
|
229
229
|
}), /*#__PURE__*/_jsx(View, {
|
|
230
|
-
nativeID: "floating-devtools-root"
|
|
231
|
-
|
|
230
|
+
nativeID: "floating-devtools-root"
|
|
231
|
+
// While the dial overlay is open, the floating menu underneath must be
|
|
232
|
+
// inert — otherwise the draggable bubble's gesture handling intercepts
|
|
233
|
+
// touches on dial controls that overlap it (e.g. the pager buttons).
|
|
234
|
+
,
|
|
235
|
+
pointerEvents: isCompletelyHidden || showDial ? "none" : "box-none",
|
|
232
236
|
style: {
|
|
233
237
|
position: "absolute",
|
|
234
238
|
top: 0,
|
|
@@ -4,6 +4,8 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
4
4
|
import { Pressable, StyleSheet, View, Dimensions, Text, Animated, Easing } from "react-native";
|
|
5
5
|
// Icons are provided by installedApps; no direct icon imports here.
|
|
6
6
|
import { DialIcon } from "./DialIcon.js";
|
|
7
|
+
import { DialPagination } from "./DialPagination.js";
|
|
8
|
+
import { getRankedToolIds, isDialUsageLoaded, loadDialUsage, recordToolUsage } from "./dialUsageStore.js";
|
|
7
9
|
import { persistentStorage, useHintsDisabled, devToolsStorageKeys, buoyColors } from "@buoy-gg/shared-ui";
|
|
8
10
|
import { DevToolsSettingsModal, useDevToolsSettings } from "../DevToolsSettingsModal";
|
|
9
11
|
import { useIsPro } from "@buoy-gg/license";
|
|
@@ -12,7 +14,8 @@ import { OnboardingTooltip } from "./OnboardingTooltip.js";
|
|
|
12
14
|
import { getDialLayout, MAX_DIAL_SLOTS, dialAnimationConfig, dialColors } from "@buoy-gg/floating-tools-core";
|
|
13
15
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
14
16
|
const {
|
|
15
|
-
width: SCREEN_WIDTH
|
|
17
|
+
width: SCREEN_WIDTH,
|
|
18
|
+
height: SCREEN_HEIGHT
|
|
16
19
|
} = Dimensions.get("window");
|
|
17
20
|
|
|
18
21
|
// Use shared layout calculation from core
|
|
@@ -22,10 +25,17 @@ const layout = getDialLayout({
|
|
|
22
25
|
const CIRCLE_SIZE = layout.circleSize;
|
|
23
26
|
const BUTTON_SIZE = layout.buttonSize;
|
|
24
27
|
const ONBOARDING_STORAGE_KEY = "@react_buoy_settings_tooltip_shown";
|
|
28
|
+
/** A non-interactive placeholder used to fill out the last dial page. */
|
|
29
|
+
const createEmptySlot = slotIndex => ({
|
|
30
|
+
id: `empty-${slotIndex}`,
|
|
31
|
+
name: `empty-${slotIndex}`,
|
|
32
|
+
icon: null,
|
|
33
|
+
color: "transparent",
|
|
34
|
+
onPress: () => {}
|
|
35
|
+
});
|
|
25
36
|
export const DialDevTools = ({
|
|
26
37
|
onClose,
|
|
27
38
|
onSettingsPress,
|
|
28
|
-
settings: externalSettings,
|
|
29
39
|
autoOpenSettings = false,
|
|
30
40
|
apps,
|
|
31
41
|
state,
|
|
@@ -38,18 +48,12 @@ export const DialDevTools = ({
|
|
|
38
48
|
const onboardingDismissedRef = useRef(false); // Track if onboarding was dismissed
|
|
39
49
|
const hintsDisabled = useHintsDisabled();
|
|
40
50
|
const {
|
|
41
|
-
settings: hookSettings,
|
|
42
51
|
refreshSettings
|
|
43
52
|
} = useDevToolsSettings();
|
|
44
53
|
const {
|
|
45
54
|
open
|
|
46
55
|
} = useAppHost();
|
|
47
56
|
const isPro = useIsPro();
|
|
48
|
-
// Initialize with external settings if provided, otherwise use hook settings
|
|
49
|
-
const [localSettings, setLocalSettings] = useState(externalSettings || hookSettings);
|
|
50
|
-
|
|
51
|
-
// Always use localSettings (which can be updated by the modal)
|
|
52
|
-
const settings = localSettings;
|
|
53
57
|
|
|
54
58
|
// Load persisted settings modal state on mount
|
|
55
59
|
useEffect(() => {
|
|
@@ -78,20 +82,6 @@ export const DialDevTools = ({
|
|
|
78
82
|
});
|
|
79
83
|
}, [isSettingsModalOpen, settingsModalStateLoaded]);
|
|
80
84
|
|
|
81
|
-
// Update local settings when external settings change
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
if (externalSettings) {
|
|
84
|
-
setLocalSettings(externalSettings);
|
|
85
|
-
}
|
|
86
|
-
}, [externalSettings]);
|
|
87
|
-
|
|
88
|
-
// Update local settings when hook settings change (if no external settings)
|
|
89
|
-
useEffect(() => {
|
|
90
|
-
if (!externalSettings) {
|
|
91
|
-
setLocalSettings(hookSettings);
|
|
92
|
-
}
|
|
93
|
-
}, [hookSettings, externalSettings]);
|
|
94
|
-
|
|
95
85
|
// Auto-open settings modal when prop is true
|
|
96
86
|
useEffect(() => {
|
|
97
87
|
if (autoOpenSettings && !isSettingsModalOpen) {
|
|
@@ -128,7 +118,6 @@ export const DialDevTools = ({
|
|
|
128
118
|
const dialRotation = useRef(new Animated.Value(0)).current;
|
|
129
119
|
const centerButtonScale = useRef(new Animated.Value(0)).current;
|
|
130
120
|
const iconsProgress = useRef(new Animated.Value(0)).current;
|
|
131
|
-
const glitchOffset = useRef(new Animated.Value(0)).current;
|
|
132
121
|
const pulseScale = useRef(new Animated.Value(1)).current;
|
|
133
122
|
const availableApps = useMemo(() => apps.map(({
|
|
134
123
|
id,
|
|
@@ -142,88 +131,111 @@ export const DialDevTools = ({
|
|
|
142
131
|
description
|
|
143
132
|
})), [apps]);
|
|
144
133
|
|
|
145
|
-
// Subtle animations
|
|
146
|
-
const floatingAnim = useRef(new Animated.Value(0)).current;
|
|
147
|
-
const breathingScale = useRef(new Animated.Value(1)).current;
|
|
148
|
-
const circuitOpacity = useRef(new Animated.Value(0)).current;
|
|
149
|
-
|
|
150
134
|
// Animation tracking refs
|
|
151
|
-
const glitchIntervalRef = useRef(null);
|
|
152
135
|
const pulseAnimationRef = useRef(null);
|
|
153
136
|
|
|
154
|
-
//
|
|
155
|
-
|
|
137
|
+
// Dial-eligible apps: everything except row-only tools. All of them are
|
|
138
|
+
// shown — paginated across pages of MAX_DIAL_SLOTS — so there is no longer
|
|
139
|
+
// a per-tool show/hide setting.
|
|
140
|
+
const dialApps = useMemo(() => apps.filter(a => (a.slot ?? "both") !== "row"), [apps]);
|
|
156
141
|
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
142
|
+
// Build a stable IconType for every dial-eligible app, keyed by id.
|
|
143
|
+
const iconsById = useMemo(() => {
|
|
144
|
+
const map = new Map();
|
|
145
|
+
for (const app of dialApps) {
|
|
146
|
+
map.set(app.id, {
|
|
147
|
+
id: app.id,
|
|
148
|
+
name: app.name,
|
|
149
|
+
// Pass both the pre-rendered icon (for non-function icons) and the
|
|
150
|
+
// component (for dynamic rendering).
|
|
151
|
+
icon: typeof app.icon === "function" ? null : app.icon,
|
|
152
|
+
// Cast to the expected type - the signature is compatible at runtime.
|
|
153
|
+
iconComponent: typeof app.icon === "function" ? app.icon : undefined,
|
|
154
|
+
color: app.color ?? buoyColors.primary,
|
|
155
|
+
onPress: () => {
|
|
156
|
+
// Record usage so frequently/recently used tools rank toward page 1.
|
|
157
|
+
void recordToolUsage(app.id);
|
|
158
|
+
|
|
159
|
+
// Call the app's onPress callback if provided, passing actions for
|
|
160
|
+
// toggle tools.
|
|
161
|
+
app?.onPress?.(actions);
|
|
162
|
+
|
|
163
|
+
// Only open modal if not a toggle-only tool.
|
|
164
|
+
if (app.launchMode !== "toggle-only") {
|
|
165
|
+
const resolvedIcon = typeof app.icon === "function" ? app.icon({
|
|
166
|
+
slot: "dial",
|
|
167
|
+
size: 20
|
|
168
|
+
}) : app.icon;
|
|
169
|
+
open({
|
|
170
|
+
id: app.id,
|
|
171
|
+
title: app.name,
|
|
172
|
+
component: app.component,
|
|
173
|
+
props: app.props,
|
|
174
|
+
launchMode: app.launchMode ?? "self-modal",
|
|
175
|
+
singleton: app.singleton ?? true,
|
|
176
|
+
icon: resolvedIcon,
|
|
177
|
+
color: app.color
|
|
178
|
+
});
|
|
179
|
+
}
|
|
193
180
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const resolvedIcon = typeof app.icon === "function" ? app.icon({
|
|
197
|
-
slot: "dial",
|
|
198
|
-
size: 20
|
|
199
|
-
}) : app.icon;
|
|
200
|
-
open({
|
|
201
|
-
id: app.id,
|
|
202
|
-
title: app.name,
|
|
203
|
-
component: app.component,
|
|
204
|
-
props: app.props,
|
|
205
|
-
launchMode: app.launchMode ?? "self-modal",
|
|
206
|
-
singleton: app.singleton ?? true,
|
|
207
|
-
icon: resolvedIcon,
|
|
208
|
-
color: app.color
|
|
209
|
-
});
|
|
181
|
+
// Close the dial.
|
|
182
|
+
onClose?.();
|
|
210
183
|
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return map;
|
|
187
|
+
}, [dialApps, actions, open, onClose]);
|
|
211
188
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
// More tools enabled than can be shown - they will be hidden
|
|
189
|
+
// Snapshot the usage-ranked order when the dial opens. It stays stable while
|
|
190
|
+
// open so icons don't jump positions mid-interaction.
|
|
191
|
+
const [rankedIds, setRankedIds] = useState(() => getRankedToolIds(dialApps.map(a => a.id)));
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
const ids = dialApps.map(a => a.id);
|
|
194
|
+
if (isDialUsageLoaded()) {
|
|
195
|
+
setRankedIds(getRankedToolIds(ids));
|
|
196
|
+
return;
|
|
221
197
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
198
|
+
// Usage data not loaded yet — show default order, then re-rank once ready.
|
|
199
|
+
let cancelled = false;
|
|
200
|
+
loadDialUsage().then(() => {
|
|
201
|
+
if (!cancelled) setRankedIds(getRankedToolIds(ids));
|
|
202
|
+
});
|
|
203
|
+
return () => {
|
|
204
|
+
cancelled = true;
|
|
205
|
+
};
|
|
206
|
+
}, [dialApps]);
|
|
207
|
+
const pageCount = Math.max(1, Math.ceil(rankedIds.length / MAX_DIAL_SLOTS));
|
|
208
|
+
const [currentPage, setCurrentPage] = useState(0);
|
|
209
|
+
const safePage = Math.min(currentPage, pageCount - 1);
|
|
210
|
+
|
|
211
|
+
// Every dial-eligible icon, with the page/slot it occupies. The ranking is
|
|
212
|
+
// snapshotted on open, so each icon's page and slot are fixed for the
|
|
213
|
+
// session — which lets us mount all icons once and paginate purely by
|
|
214
|
+
// toggling visibility (no remounts on page change).
|
|
215
|
+
const allDialIcons = useMemo(() => {
|
|
216
|
+
return rankedIds.map(id => iconsById.get(id)).filter(icon => Boolean(icon)).map((icon, i) => ({
|
|
217
|
+
icon,
|
|
218
|
+
page: Math.floor(i / MAX_DIAL_SLOTS),
|
|
219
|
+
slot: i % MAX_DIAL_SLOTS
|
|
220
|
+
}));
|
|
221
|
+
}, [rankedIds, iconsById]);
|
|
222
|
+
|
|
223
|
+
// Empty slot indices for the current page (only the last page can be
|
|
224
|
+
// partial). These are cheap placeholder dots.
|
|
225
|
+
const emptySlots = useMemo(() => {
|
|
226
|
+
const onThisPage = allDialIcons.filter(d => d.page === safePage).length;
|
|
227
|
+
const slots = [];
|
|
228
|
+
for (let s = onThisPage; s < MAX_DIAL_SLOTS; s += 1) slots.push(s);
|
|
229
|
+
return slots;
|
|
230
|
+
}, [allDialIcons, safePage]);
|
|
231
|
+
|
|
232
|
+
// Swap to another page. Every icon is already mounted, so this only toggles
|
|
233
|
+
// which ones are visible — no remount, no re-animation — keeping page
|
|
234
|
+
// changes instant.
|
|
235
|
+
const handlePageChange = next => {
|
|
236
|
+
const clamped = Math.max(0, Math.min(next, pageCount - 1));
|
|
237
|
+
if (clamped !== safePage) setCurrentPage(clamped);
|
|
238
|
+
};
|
|
227
239
|
|
|
228
240
|
// Initialize animations on mount - using shared config from core
|
|
229
241
|
useEffect(() => {
|
|
@@ -244,13 +256,13 @@ export const DialDevTools = ({
|
|
|
244
256
|
}
|
|
245
257
|
},
|
|
246
258
|
centerButton: {
|
|
247
|
-
delay:
|
|
259
|
+
delay: 150,
|
|
248
260
|
damping: 10,
|
|
249
261
|
stiffness: 200
|
|
250
262
|
},
|
|
251
263
|
icons: {
|
|
252
|
-
delay:
|
|
253
|
-
duration:
|
|
264
|
+
delay: 200,
|
|
265
|
+
duration: 400
|
|
254
266
|
},
|
|
255
267
|
circuitTraces: {
|
|
256
268
|
delay: 600,
|
|
@@ -321,81 +333,21 @@ export const DialDevTools = ({
|
|
|
321
333
|
useNativeDriver: true
|
|
322
334
|
})]).start();
|
|
323
335
|
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
duration: continuous.glitch.stepDuration,
|
|
329
|
-
useNativeDriver: true
|
|
330
|
-
}), Animated.timing(glitchOffset, {
|
|
331
|
-
toValue: -continuous.glitch.offset,
|
|
332
|
-
duration: continuous.glitch.stepDuration,
|
|
333
|
-
useNativeDriver: true
|
|
334
|
-
}), Animated.timing(glitchOffset, {
|
|
335
|
-
toValue: 0,
|
|
336
|
-
duration: continuous.glitch.stepDuration,
|
|
337
|
-
useNativeDriver: true
|
|
338
|
-
})]).start();
|
|
339
|
-
};
|
|
340
|
-
glitchIntervalRef.current = setInterval(glitchAnimation, continuous.glitch.interval);
|
|
341
|
-
|
|
342
|
-
// Pulse animation - using shared config
|
|
343
|
-
const startPulse = () => {
|
|
344
|
-
pulseAnimationRef.current = Animated.loop(Animated.sequence([Animated.timing(pulseScale, {
|
|
345
|
-
toValue: continuous.pulse.maxScale,
|
|
346
|
-
duration: continuous.pulse.duration,
|
|
347
|
-
easing: Easing.inOut(Easing.ease),
|
|
348
|
-
useNativeDriver: true
|
|
349
|
-
}), Animated.timing(pulseScale, {
|
|
350
|
-
toValue: continuous.pulse.minScale,
|
|
351
|
-
duration: continuous.pulse.duration,
|
|
352
|
-
easing: Easing.inOut(Easing.ease),
|
|
353
|
-
useNativeDriver: true
|
|
354
|
-
})]));
|
|
355
|
-
pulseAnimationRef.current.start();
|
|
356
|
-
};
|
|
357
|
-
startPulse();
|
|
358
|
-
|
|
359
|
-
// Subtle floating animation for the dial - using shared config
|
|
360
|
-
Animated.loop(Animated.sequence([Animated.timing(floatingAnim, {
|
|
361
|
-
toValue: continuous.floating.maxY,
|
|
362
|
-
duration: continuous.floating.duration,
|
|
336
|
+
// Pulse animation - only continuous effect kept for performance
|
|
337
|
+
pulseAnimationRef.current = Animated.loop(Animated.sequence([Animated.timing(pulseScale, {
|
|
338
|
+
toValue: continuous.pulse.maxScale,
|
|
339
|
+
duration: continuous.pulse.duration,
|
|
363
340
|
easing: Easing.inOut(Easing.ease),
|
|
364
341
|
useNativeDriver: true
|
|
365
|
-
}), Animated.timing(
|
|
366
|
-
toValue: continuous.
|
|
367
|
-
duration: continuous.
|
|
342
|
+
}), Animated.timing(pulseScale, {
|
|
343
|
+
toValue: continuous.pulse.minScale,
|
|
344
|
+
duration: continuous.pulse.duration,
|
|
368
345
|
easing: Easing.inOut(Easing.ease),
|
|
369
346
|
useNativeDriver: true
|
|
370
|
-
})]))
|
|
371
|
-
|
|
372
|
-
// Gentle breathing effect for center button - using shared config
|
|
373
|
-
Animated.loop(Animated.sequence([Animated.timing(breathingScale, {
|
|
374
|
-
toValue: continuous.breathing.maxScale,
|
|
375
|
-
duration: continuous.breathing.duration,
|
|
376
|
-
easing: Easing.inOut(Easing.ease),
|
|
377
|
-
useNativeDriver: true
|
|
378
|
-
}), Animated.timing(breathingScale, {
|
|
379
|
-
toValue: continuous.breathing.minScale,
|
|
380
|
-
duration: continuous.breathing.duration,
|
|
381
|
-
easing: Easing.inOut(Easing.ease),
|
|
382
|
-
useNativeDriver: true
|
|
383
|
-
})])).start();
|
|
384
|
-
|
|
385
|
-
// Circuit traces fade in - using shared config
|
|
386
|
-
Animated.timing(circuitOpacity, {
|
|
387
|
-
toValue: 1,
|
|
388
|
-
duration: entrance.circuitTraces.duration,
|
|
389
|
-
delay: entrance.circuitTraces.delay,
|
|
390
|
-
useNativeDriver: true
|
|
391
|
-
}).start();
|
|
347
|
+
})]));
|
|
348
|
+
pulseAnimationRef.current.start();
|
|
392
349
|
return () => {
|
|
393
|
-
|
|
394
|
-
clearInterval(glitchIntervalRef.current);
|
|
395
|
-
}
|
|
396
|
-
if (pulseAnimationRef.current) {
|
|
397
|
-
pulseAnimationRef.current.stop();
|
|
398
|
-
}
|
|
350
|
+
pulseAnimationRef.current?.stop();
|
|
399
351
|
};
|
|
400
352
|
}, []);
|
|
401
353
|
const handleOnboardingDismiss = () => {
|
|
@@ -467,8 +419,8 @@ export const DialDevTools = ({
|
|
|
467
419
|
}
|
|
468
420
|
});
|
|
469
421
|
};
|
|
470
|
-
const handleIconPress =
|
|
471
|
-
setSelectedIcon(
|
|
422
|
+
const handleIconPress = icon => {
|
|
423
|
+
setSelectedIcon(1);
|
|
472
424
|
const interaction = dialAnimationConfig?.interaction ?? {
|
|
473
425
|
iconSelect: {
|
|
474
426
|
pulse: [{
|
|
@@ -500,9 +452,9 @@ export const DialDevTools = ({
|
|
|
500
452
|
|
|
501
453
|
// Trigger action - using shared delay
|
|
502
454
|
setTimeout(() => {
|
|
503
|
-
|
|
455
|
+
icon.onPress();
|
|
504
456
|
// Only close if it's not the WiFi toggle (by id)
|
|
505
|
-
if (
|
|
457
|
+
if (icon.id !== "wifi") {
|
|
506
458
|
handleClose();
|
|
507
459
|
}
|
|
508
460
|
}, interaction.iconSelect.actionDelay);
|
|
@@ -512,14 +464,9 @@ export const DialDevTools = ({
|
|
|
512
464
|
const backdropAnimatedStyle = {
|
|
513
465
|
opacity: backdropOpacity
|
|
514
466
|
};
|
|
515
|
-
const glitchAnimatedStyle = {
|
|
516
|
-
transform: [{
|
|
517
|
-
translateX: glitchOffset
|
|
518
|
-
}]
|
|
519
|
-
};
|
|
520
467
|
const centerButtonAnimatedStyle = {
|
|
521
468
|
transform: [{
|
|
522
|
-
scale:
|
|
469
|
+
scale: centerButtonScale
|
|
523
470
|
}]
|
|
524
471
|
};
|
|
525
472
|
const pulseAnimatedStyle = {
|
|
@@ -542,8 +489,6 @@ export const DialDevTools = ({
|
|
|
542
489
|
left: (SCREEN_WIDTH - CIRCLE_SIZE) / 2,
|
|
543
490
|
bottom: 80,
|
|
544
491
|
transform: [{
|
|
545
|
-
translateY: floatingAnim
|
|
546
|
-
}, {
|
|
547
492
|
scale: dialScale
|
|
548
493
|
}, {
|
|
549
494
|
rotate: dialRotation.interpolate({
|
|
@@ -553,7 +498,7 @@ export const DialDevTools = ({
|
|
|
553
498
|
}]
|
|
554
499
|
}],
|
|
555
500
|
children: [/*#__PURE__*/_jsxs(Animated.View, {
|
|
556
|
-
style:
|
|
501
|
+
style: styles.circle,
|
|
557
502
|
children: [/*#__PURE__*/_jsxs(View, {
|
|
558
503
|
style: styles.gradientBackground,
|
|
559
504
|
children: [/*#__PURE__*/_jsx(View, {
|
|
@@ -574,14 +519,26 @@ export const DialDevTools = ({
|
|
|
574
519
|
}]
|
|
575
520
|
}, i))
|
|
576
521
|
})]
|
|
577
|
-
}),
|
|
578
|
-
|
|
522
|
+
}), allDialIcons.filter(({
|
|
523
|
+
page
|
|
524
|
+
}) => page === safePage).map(({
|
|
525
|
+
icon,
|
|
526
|
+
slot
|
|
527
|
+
}) => /*#__PURE__*/_jsx(DialIcon, {
|
|
579
528
|
onPress: handleIconPress,
|
|
580
529
|
iconsProgress: iconsProgress,
|
|
581
530
|
icon: icon,
|
|
582
|
-
index:
|
|
583
|
-
totalIcons:
|
|
584
|
-
|
|
531
|
+
index: slot,
|
|
532
|
+
totalIcons: MAX_DIAL_SLOTS,
|
|
533
|
+
active: true
|
|
534
|
+
}, icon.id ?? `page${safePage}-${slot}`)), emptySlots.map(slot => /*#__PURE__*/_jsx(DialIcon, {
|
|
535
|
+
onPress: handleIconPress,
|
|
536
|
+
iconsProgress: iconsProgress,
|
|
537
|
+
icon: createEmptySlot(slot),
|
|
538
|
+
index: slot,
|
|
539
|
+
totalIcons: MAX_DIAL_SLOTS,
|
|
540
|
+
active: true
|
|
541
|
+
}, `empty-${slot}`))]
|
|
585
542
|
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
586
543
|
style: [styles.buttonContainer, centerButtonAnimatedStyle],
|
|
587
544
|
children: /*#__PURE__*/_jsxs(View, {
|
|
@@ -639,19 +596,32 @@ export const DialDevTools = ({
|
|
|
639
596
|
})]
|
|
640
597
|
})
|
|
641
598
|
})]
|
|
642
|
-
}), /*#__PURE__*/_jsx(
|
|
643
|
-
|
|
599
|
+
}), pageCount > 1 && /*#__PURE__*/_jsx(DialPagination, {
|
|
600
|
+
page: safePage,
|
|
601
|
+
pageCount: pageCount,
|
|
602
|
+
onPrev: () => handlePageChange(safePage - 1),
|
|
603
|
+
onNext: () => handlePageChange(safePage + 1),
|
|
604
|
+
animatedStyle: {
|
|
605
|
+
position: "absolute",
|
|
606
|
+
left: (SCREEN_WIDTH - CIRCLE_SIZE) / 2,
|
|
607
|
+
// Circle's bottom edge sits at bottom: 80 -> SCREEN_HEIGHT - 80
|
|
608
|
+
// from the top. Place the pager 16px below that edge.
|
|
609
|
+
top: SCREEN_HEIGHT - 80 + 16,
|
|
610
|
+
width: CIRCLE_SIZE,
|
|
611
|
+
opacity: dialScale,
|
|
612
|
+
transform: [{
|
|
613
|
+
scale: dialScale
|
|
614
|
+
}]
|
|
615
|
+
}
|
|
616
|
+
}), isSettingsModalOpen && /*#__PURE__*/_jsx(DevToolsSettingsModal, {
|
|
617
|
+
visible: true,
|
|
644
618
|
onClose: () => {
|
|
645
619
|
setIsSettingsModalOpen(false);
|
|
646
620
|
refreshSettings(); // Refresh from storage
|
|
647
621
|
},
|
|
648
|
-
onSettingsChange: newSettings => {
|
|
649
|
-
// Immediately update local settings for instant feedback
|
|
650
|
-
setLocalSettings(newSettings);
|
|
651
|
-
},
|
|
652
622
|
availableApps: availableApps
|
|
653
|
-
}), /*#__PURE__*/_jsx(OnboardingTooltip, {
|
|
654
|
-
visible:
|
|
623
|
+
}), showOnboardingTooltip && !isSettingsModalOpen && !onboardingDismissedRef.current && /*#__PURE__*/_jsx(OnboardingTooltip, {
|
|
624
|
+
visible: true,
|
|
655
625
|
onDismiss: handleOnboardingDismiss
|
|
656
626
|
})]
|
|
657
627
|
});
|
|
@@ -211,8 +211,14 @@ export function DialMenu({
|
|
|
211
211
|
}), []);
|
|
212
212
|
const gridRotations = useMemo(() => getGridLineRotations(), []);
|
|
213
213
|
const positions = useMemo(() => getAllIconPositions(MAX_DIAL_SLOTS, layout.iconRadius), [layout.iconRadius]);
|
|
214
|
+
|
|
215
|
+
// Pagination: tools are split across pages of MAX_DIAL_SLOTS.
|
|
216
|
+
const [currentPage, setCurrentPage] = useState(0);
|
|
217
|
+
const pageCount = Math.max(1, Math.ceil(icons.length / MAX_DIAL_SLOTS));
|
|
218
|
+
const safePage = Math.min(currentPage, pageCount - 1);
|
|
214
219
|
const paddedIcons = useMemo(() => {
|
|
215
|
-
const
|
|
220
|
+
const start = safePage * MAX_DIAL_SLOTS;
|
|
221
|
+
const result = [...icons.slice(start, start + MAX_DIAL_SLOTS)];
|
|
216
222
|
while (result.length < MAX_DIAL_SLOTS) {
|
|
217
223
|
result.push({
|
|
218
224
|
id: `empty-${result.length}`,
|
|
@@ -222,7 +228,7 @@ export function DialMenu({
|
|
|
222
228
|
});
|
|
223
229
|
}
|
|
224
230
|
return result;
|
|
225
|
-
}, [icons]);
|
|
231
|
+
}, [icons, safePage]);
|
|
226
232
|
|
|
227
233
|
// Inject keyframes
|
|
228
234
|
useEffect(() => {
|
|
@@ -399,10 +405,37 @@ export function DialMenu({
|
|
|
399
405
|
}, interaction.iconSelect.actionDelay);
|
|
400
406
|
}, [interaction.iconSelect, handleClose]);
|
|
401
407
|
|
|
408
|
+
// Page navigation - icons are keyed by slot index, so this only swaps
|
|
409
|
+
// their content in place (no remount, no re-animation) for an instant page
|
|
410
|
+
// change.
|
|
411
|
+
const handlePageChange = useCallback(next => {
|
|
412
|
+
if (isClosingRef.current) return;
|
|
413
|
+
const clamped = Math.max(0, Math.min(next, pageCount - 1));
|
|
414
|
+
if (clamped !== safePage) setCurrentPage(clamped);
|
|
415
|
+
}, [pageCount, safePage]);
|
|
416
|
+
|
|
402
417
|
// Computed values
|
|
403
418
|
const buttonContainerSize = layout.buttonSize * dialStyles.centerButton.containerRatio;
|
|
404
419
|
const buttonBorderSize = layout.buttonSize * dialStyles.centerButton.borderRatio;
|
|
405
420
|
const isAnimating = entranceComplete && !isExiting;
|
|
421
|
+
const pagerButtonStyle = disabled => ({
|
|
422
|
+
display: 'flex',
|
|
423
|
+
alignItems: 'center',
|
|
424
|
+
gap: 4,
|
|
425
|
+
padding: '8px 16px',
|
|
426
|
+
borderRadius: 10,
|
|
427
|
+
border: `1px solid ${dialColors.dialBorder}`,
|
|
428
|
+
backgroundColor: dialColors.dialBackground,
|
|
429
|
+
color: disabled ? dialColors.emptyDotBorder : dialColors.dialShadow,
|
|
430
|
+
fontSize: 12,
|
|
431
|
+
fontWeight: 900,
|
|
432
|
+
fontFamily: 'monospace',
|
|
433
|
+
letterSpacing: 1.5,
|
|
434
|
+
textTransform: 'uppercase',
|
|
435
|
+
cursor: disabled ? 'default' : 'pointer',
|
|
436
|
+
opacity: disabled ? 0.4 : 1,
|
|
437
|
+
boxShadow: disabled ? 'none' : `0 0 8px ${dialColors.dialShadow}66`
|
|
438
|
+
});
|
|
406
439
|
return /*#__PURE__*/_jsxs("div", {
|
|
407
440
|
role: "dialog",
|
|
408
441
|
"aria-label": "Dial Menu",
|
|
@@ -427,11 +460,15 @@ export function DialMenu({
|
|
|
427
460
|
backgroundColor: dialColors.dialBackdrop,
|
|
428
461
|
opacity: backdropOpacity
|
|
429
462
|
}
|
|
430
|
-
}), /*#__PURE__*/
|
|
463
|
+
}), /*#__PURE__*/_jsxs("div", {
|
|
431
464
|
style: {
|
|
432
|
-
animation: isAnimating ? cssAnimations.floating : 'none'
|
|
465
|
+
animation: isAnimating ? cssAnimations.floating : 'none',
|
|
466
|
+
display: 'flex',
|
|
467
|
+
flexDirection: 'column',
|
|
468
|
+
alignItems: 'center',
|
|
469
|
+
gap: 16
|
|
433
470
|
},
|
|
434
|
-
children: /*#__PURE__*/_jsx("div", {
|
|
471
|
+
children: [/*#__PURE__*/_jsx("div", {
|
|
435
472
|
style: {
|
|
436
473
|
animation: triggerGlitch > 0 && isAnimating ? cssAnimations.glitch : 'none'
|
|
437
474
|
},
|
|
@@ -494,7 +531,7 @@ export function DialMenu({
|
|
|
494
531
|
position: positions[index],
|
|
495
532
|
progress: iconProgress,
|
|
496
533
|
onPress: () => handleIconPress(icon)
|
|
497
|
-
},
|
|
534
|
+
}, index)), /*#__PURE__*/_jsxs("div", {
|
|
498
535
|
style: {
|
|
499
536
|
position: 'absolute',
|
|
500
537
|
left: '50%',
|
|
@@ -584,7 +621,45 @@ export function DialMenu({
|
|
|
584
621
|
})]
|
|
585
622
|
})]
|
|
586
623
|
})
|
|
587
|
-
}, triggerGlitch)
|
|
624
|
+
}, triggerGlitch), pageCount > 1 && /*#__PURE__*/_jsxs("div", {
|
|
625
|
+
style: {
|
|
626
|
+
display: 'flex',
|
|
627
|
+
alignItems: 'center',
|
|
628
|
+
gap: 12,
|
|
629
|
+
opacity: Math.min(1, dialScale)
|
|
630
|
+
},
|
|
631
|
+
children: [/*#__PURE__*/_jsx("button", {
|
|
632
|
+
type: "button",
|
|
633
|
+
"aria-label": "Previous dial page",
|
|
634
|
+
disabled: safePage <= 0,
|
|
635
|
+
onClick: () => handlePageChange(safePage - 1),
|
|
636
|
+
style: pagerButtonStyle(safePage <= 0),
|
|
637
|
+
children: "\u2039 PREV"
|
|
638
|
+
}), /*#__PURE__*/_jsxs("span", {
|
|
639
|
+
style: {
|
|
640
|
+
fontSize: 13,
|
|
641
|
+
fontWeight: 900,
|
|
642
|
+
fontFamily: 'monospace',
|
|
643
|
+
letterSpacing: 2,
|
|
644
|
+
color: '#FFFFFF',
|
|
645
|
+
textShadow: `0 0 6px ${dialColors.dialShadow}`
|
|
646
|
+
},
|
|
647
|
+
children: [String(safePage + 1).padStart(2, '0'), /*#__PURE__*/_jsxs("span", {
|
|
648
|
+
style: {
|
|
649
|
+
color: dialColors.iconLabel,
|
|
650
|
+
textShadow: 'none'
|
|
651
|
+
},
|
|
652
|
+
children: [' / ', String(pageCount).padStart(2, '0')]
|
|
653
|
+
})]
|
|
654
|
+
}), /*#__PURE__*/_jsx("button", {
|
|
655
|
+
type: "button",
|
|
656
|
+
"aria-label": "Next dial page",
|
|
657
|
+
disabled: safePage >= pageCount - 1,
|
|
658
|
+
onClick: () => handlePageChange(safePage + 1),
|
|
659
|
+
style: pagerButtonStyle(safePage >= pageCount - 1),
|
|
660
|
+
children: "NEXT \u203A"
|
|
661
|
+
})]
|
|
662
|
+
})]
|
|
588
663
|
})]
|
|
589
664
|
});
|
|
590
665
|
}
|