@buoy-gg/core 2.1.15 → 3.0.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 (55) hide show
  1. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.js +4 -34
  2. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.web.js +3 -25
  3. package/lib/commonjs/floatingMenu/FloatingDevTools.js +14 -1
  4. package/lib/commonjs/floatingMenu/FloatingDevTools.web.js +19 -9
  5. package/lib/commonjs/floatingMenu/FloatingMenu.js +6 -6
  6. package/lib/commonjs/floatingMenu/defaultConfig.js +1 -1
  7. package/lib/commonjs/floatingMenu/dial/DialDevTools.js +206 -224
  8. package/lib/commonjs/floatingMenu/dial/DialDevTools.web.js +82 -7
  9. package/lib/commonjs/floatingMenu/dial/DialIcon.js +77 -71
  10. package/lib/commonjs/floatingMenu/dial/DialPagination.js +170 -0
  11. package/lib/commonjs/floatingMenu/dial/dialUsageStore.js +97 -0
  12. package/lib/module/floatingMenu/DevToolsSettingsModal.js +5 -35
  13. package/lib/module/floatingMenu/DevToolsSettingsModal.web.js +4 -28
  14. package/lib/module/floatingMenu/FloatingDevTools.js +14 -1
  15. package/lib/module/floatingMenu/FloatingDevTools.web.js +19 -9
  16. package/lib/module/floatingMenu/FloatingMenu.js +7 -7
  17. package/lib/module/floatingMenu/defaultConfig.js +1 -1
  18. package/lib/module/floatingMenu/dial/DialDevTools.js +209 -226
  19. package/lib/module/floatingMenu/dial/DialDevTools.web.js +82 -7
  20. package/lib/module/floatingMenu/dial/DialIcon.js +81 -74
  21. package/lib/module/floatingMenu/dial/DialPagination.js +165 -0
  22. package/lib/module/floatingMenu/dial/dialUsageStore.js +89 -0
  23. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
  24. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -1
  25. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/floatingMenu/FloatingMenu.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts +1 -1
  29. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts.map +1 -1
  30. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts +0 -2
  31. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/floatingMenu/dial/DialIcon.d.ts +7 -2
  34. package/lib/typescript/commonjs/floatingMenu/dial/DialIcon.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/floatingMenu/dial/DialPagination.d.ts +22 -0
  36. package/lib/typescript/commonjs/floatingMenu/dial/DialPagination.d.ts.map +1 -0
  37. package/lib/typescript/commonjs/floatingMenu/dial/dialUsageStore.d.ts +34 -0
  38. package/lib/typescript/commonjs/floatingMenu/dial/dialUsageStore.d.ts.map +1 -0
  39. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
  40. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -1
  41. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  42. package/lib/typescript/module/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
  43. package/lib/typescript/module/floatingMenu/FloatingMenu.d.ts.map +1 -1
  44. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts +1 -1
  45. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts.map +1 -1
  46. package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts +0 -2
  47. package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  48. package/lib/typescript/module/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -1
  49. package/lib/typescript/module/floatingMenu/dial/DialIcon.d.ts +7 -2
  50. package/lib/typescript/module/floatingMenu/dial/DialIcon.d.ts.map +1 -1
  51. package/lib/typescript/module/floatingMenu/dial/DialPagination.d.ts +22 -0
  52. package/lib/typescript/module/floatingMenu/dial/DialPagination.d.ts.map +1 -0
  53. package/lib/typescript/module/floatingMenu/dial/dialUsageStore.d.ts +34 -0
  54. package/lib/typescript/module/floatingMenu/dial/dialUsageStore.d.ts.map +1 -0
  55. package/package.json +5 -5
@@ -7,6 +7,8 @@ exports.DialDevTools = void 0;
7
7
  var _react = require("react");
8
8
  var _reactNative = require("react-native");
9
9
  var _DialIcon = require("./DialIcon.js");
10
+ var _DialPagination = require("./DialPagination.js");
11
+ var _dialUsageStore = require("./dialUsageStore.js");
10
12
  var _sharedUi = require("@buoy-gg/shared-ui");
11
13
  var _DevToolsSettingsModal = require("../DevToolsSettingsModal");
12
14
  var _license = require("@buoy-gg/license");
@@ -16,21 +18,22 @@ var _floatingToolsCore = require("@buoy-gg/floating-tools-core");
16
18
  var _jsxRuntime = require("react/jsx-runtime");
17
19
  // Icons are provided by installedApps; no direct icon imports here.
18
20
 
19
- const {
20
- width: SCREEN_WIDTH
21
- } = _reactNative.Dimensions.get("window");
22
-
23
- // Use shared layout calculation from core
24
- const layout = (0, _floatingToolsCore.getDialLayout)({
25
- screenWidth: SCREEN_WIDTH
26
- });
27
- const CIRCLE_SIZE = layout.circleSize;
28
- const BUTTON_SIZE = layout.buttonSize;
21
+ // The circle size depends on the live window width, so it's computed inside
22
+ // the component via useWindowDimensions — a module-scope Dimensions.get
23
+ // snapshot goes stale when the window resizes after load (web/desktop).
24
+ const BUTTON_SIZE = _floatingToolsCore.DIAL_BUTTON_SIZE;
29
25
  const ONBOARDING_STORAGE_KEY = "@react_buoy_settings_tooltip_shown";
26
+ /** A non-interactive placeholder used to fill out the last dial page. */
27
+ const createEmptySlot = slotIndex => ({
28
+ id: `empty-${slotIndex}`,
29
+ name: `empty-${slotIndex}`,
30
+ icon: null,
31
+ color: "transparent",
32
+ onPress: () => {}
33
+ });
30
34
  const DialDevTools = ({
31
35
  onClose,
32
36
  onSettingsPress,
33
- settings: externalSettings,
34
37
  autoOpenSettings = false,
35
38
  apps,
36
39
  state,
@@ -43,18 +46,39 @@ const DialDevTools = ({
43
46
  const onboardingDismissedRef = (0, _react.useRef)(false); // Track if onboarding was dismissed
44
47
  const hintsDisabled = (0, _sharedUi.useHintsDisabled)();
45
48
  const {
46
- settings: hookSettings,
47
49
  refreshSettings
48
50
  } = (0, _DevToolsSettingsModal.useDevToolsSettings)();
49
51
  const {
50
52
  open
51
53
  } = (0, _AppHost.useAppHost)();
52
54
  const isPro = (0, _license.useIsPro)();
53
- // Initialize with external settings if provided, otherwise use hook settings
54
- const [localSettings, setLocalSettings] = (0, _react.useState)(externalSettings || hookSettings);
55
55
 
56
- // Always use localSettings (which can be updated by the modal)
57
- const settings = localSettings;
56
+ // Live window size keeps the dial centered and sized correctly when the
57
+ // window resizes (Electron/web) or the device rotates.
58
+ const {
59
+ width: screenWidth,
60
+ height: screenHeight
61
+ } = (0, _reactNative.useWindowDimensions)();
62
+ const circleSize = (0, _floatingToolsCore.getDialLayout)({
63
+ screenWidth
64
+ }).circleSize;
65
+ const sizeStyles = (0, _react.useMemo)(() => ({
66
+ parent: {
67
+ width: circleSize,
68
+ height: circleSize
69
+ },
70
+ circle: {
71
+ width: circleSize,
72
+ height: circleSize,
73
+ borderRadius: circleSize / 2
74
+ },
75
+ rounded: {
76
+ borderRadius: circleSize / 2
77
+ },
78
+ gridLine: {
79
+ width: circleSize
80
+ }
81
+ }), [circleSize]);
58
82
 
59
83
  // Load persisted settings modal state on mount
60
84
  (0, _react.useEffect)(() => {
@@ -83,20 +107,6 @@ const DialDevTools = ({
83
107
  });
84
108
  }, [isSettingsModalOpen, settingsModalStateLoaded]);
85
109
 
86
- // Update local settings when external settings change
87
- (0, _react.useEffect)(() => {
88
- if (externalSettings) {
89
- setLocalSettings(externalSettings);
90
- }
91
- }, [externalSettings]);
92
-
93
- // Update local settings when hook settings change (if no external settings)
94
- (0, _react.useEffect)(() => {
95
- if (!externalSettings) {
96
- setLocalSettings(hookSettings);
97
- }
98
- }, [hookSettings, externalSettings]);
99
-
100
110
  // Auto-open settings modal when prop is true
101
111
  (0, _react.useEffect)(() => {
102
112
  if (autoOpenSettings && !isSettingsModalOpen) {
@@ -133,7 +143,6 @@ const DialDevTools = ({
133
143
  const dialRotation = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current;
134
144
  const centerButtonScale = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current;
135
145
  const iconsProgress = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current;
136
- const glitchOffset = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current;
137
146
  const pulseScale = (0, _react.useRef)(new _reactNative.Animated.Value(1)).current;
138
147
  const availableApps = (0, _react.useMemo)(() => apps.map(({
139
148
  id,
@@ -147,88 +156,111 @@ const DialDevTools = ({
147
156
  description
148
157
  })), [apps]);
149
158
 
150
- // Subtle animations
151
- const floatingAnim = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current;
152
- const breathingScale = (0, _react.useRef)(new _reactNative.Animated.Value(1)).current;
153
- const circuitOpacity = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current;
154
-
155
159
  // Animation tracking refs
156
- const glitchIntervalRef = (0, _react.useRef)(null);
157
160
  const pulseAnimationRef = (0, _react.useRef)(null);
158
161
 
159
- // Map data-driven apps to dial icons, inserting empty slots for disabled items
160
- const dialApps = apps.filter(a => (a.slot ?? "both") !== "row");
162
+ // Dial-eligible apps: everything except row-only tools. All of them are
163
+ // shown paginated across pages of MAX_DIAL_SLOTS so there is no longer
164
+ // a per-tool show/hide setting.
165
+ const dialApps = (0, _react.useMemo)(() => apps.filter(a => (a.slot ?? "both") !== "row"), [apps]);
161
166
 
162
- // Check if settings are "virgin" (user hasn't customized dial tools yet)
163
- // If no explicit settings exist, auto-enable all dial tools by default
164
- const dialToolsKeys = settings?.dialTools ? Object.keys(settings.dialTools) : [];
165
- const hasDialSettings = dialToolsKeys.length > 0;
166
- const isDialEnabled = id => {
167
- // No settings or empty dialTools = auto-enable all dial tools by default
168
- if (!settings?.dialTools || !hasDialSettings) return true;
169
- return settings.dialTools[id] ?? false;
170
- };
171
- const createEmptySlot = slotIndex => ({
172
- id: `empty-${slotIndex}`,
173
- name: `empty-${slotIndex}`,
174
- icon: null,
175
- color: "transparent",
176
- onPress: () => {}
177
- });
178
- const enabledIcons = [];
179
- for (const app of dialApps) {
180
- if (!isDialEnabled(app.id)) {
181
- continue;
182
- }
183
- if (enabledIcons.length >= _floatingToolsCore.MAX_DIAL_SLOTS) {
184
- break;
185
- }
186
- enabledIcons.push({
187
- id: app.id,
188
- name: app.name,
189
- // Pass both the pre-rendered icon (for non-function icons) and the component (for dynamic rendering)
190
- icon: typeof app.icon === "function" ? null // Will be rendered dynamically by DialIcon
191
- : app.icon,
192
- // Cast to the expected type - the function signature is compatible at runtime
193
- iconComponent: typeof app.icon === "function" ? app.icon : undefined,
194
- color: app.color ?? _sharedUi.buoyColors.primary,
195
- onPress: () => {
196
- // Call the app's onPress callback if provided, passing actions for toggle tools
197
- app?.onPress?.(actions);
167
+ // Build a stable IconType for every dial-eligible app, keyed by id.
168
+ const iconsById = (0, _react.useMemo)(() => {
169
+ const map = new Map();
170
+ for (const app of dialApps) {
171
+ map.set(app.id, {
172
+ id: app.id,
173
+ name: app.name,
174
+ // Pass both the pre-rendered icon (for non-function icons) and the
175
+ // component (for dynamic rendering).
176
+ icon: typeof app.icon === "function" ? null : app.icon,
177
+ // Cast to the expected type - the signature is compatible at runtime.
178
+ iconComponent: typeof app.icon === "function" ? app.icon : undefined,
179
+ color: app.color ?? _sharedUi.buoyColors.primary,
180
+ onPress: () => {
181
+ // Record usage so frequently/recently used tools rank toward page 1.
182
+ void (0, _dialUsageStore.recordToolUsage)(app.id);
183
+
184
+ // Call the app's onPress callback if provided, passing actions for
185
+ // toggle tools.
186
+ app?.onPress?.(actions);
187
+
188
+ // Only open modal if not a toggle-only tool.
189
+ if (app.launchMode !== "toggle-only") {
190
+ const resolvedIcon = typeof app.icon === "function" ? app.icon({
191
+ slot: "dial",
192
+ size: 20
193
+ }) : app.icon;
194
+ open({
195
+ id: app.id,
196
+ title: app.name,
197
+ component: app.component,
198
+ props: app.props,
199
+ launchMode: app.launchMode ?? "self-modal",
200
+ singleton: app.singleton ?? true,
201
+ icon: resolvedIcon,
202
+ color: app.color
203
+ });
204
+ }
198
205
 
199
- // Only open modal if not a toggle-only tool
200
- if (app.launchMode !== "toggle-only") {
201
- const resolvedIcon = typeof app.icon === "function" ? app.icon({
202
- slot: "dial",
203
- size: 20
204
- }) : app.icon;
205
- open({
206
- id: app.id,
207
- title: app.name,
208
- component: app.component,
209
- props: app.props,
210
- launchMode: app.launchMode ?? "self-modal",
211
- singleton: app.singleton ?? true,
212
- icon: resolvedIcon,
213
- color: app.color
214
- });
206
+ // Close the dial.
207
+ onClose?.();
215
208
  }
209
+ });
210
+ }
211
+ return map;
212
+ }, [dialApps, actions, open, onClose]);
216
213
 
217
- // Close the dial
218
- onClose?.();
219
- }
220
- });
221
- }
222
- if (__DEV__) {
223
- const totalEnabled = dialApps.filter(app => isDialEnabled(app.id)).length;
224
- if (totalEnabled > _floatingToolsCore.MAX_DIAL_SLOTS) {
225
- // More tools enabled than can be shown - they will be hidden
214
+ // Snapshot the usage-ranked order when the dial opens. It stays stable while
215
+ // open so icons don't jump positions mid-interaction.
216
+ const [rankedIds, setRankedIds] = (0, _react.useState)(() => (0, _dialUsageStore.getRankedToolIds)(dialApps.map(a => a.id)));
217
+ (0, _react.useEffect)(() => {
218
+ const ids = dialApps.map(a => a.id);
219
+ if ((0, _dialUsageStore.isDialUsageLoaded)()) {
220
+ setRankedIds((0, _dialUsageStore.getRankedToolIds)(ids));
221
+ return;
226
222
  }
227
- }
228
- const icons = [...enabledIcons];
229
- while (icons.length < _floatingToolsCore.MAX_DIAL_SLOTS) {
230
- icons.push(createEmptySlot(icons.length));
231
- }
223
+ // Usage data not loaded yet — show default order, then re-rank once ready.
224
+ let cancelled = false;
225
+ (0, _dialUsageStore.loadDialUsage)().then(() => {
226
+ if (!cancelled) setRankedIds((0, _dialUsageStore.getRankedToolIds)(ids));
227
+ });
228
+ return () => {
229
+ cancelled = true;
230
+ };
231
+ }, [dialApps]);
232
+ const pageCount = Math.max(1, Math.ceil(rankedIds.length / _floatingToolsCore.MAX_DIAL_SLOTS));
233
+ const [currentPage, setCurrentPage] = (0, _react.useState)(0);
234
+ const safePage = Math.min(currentPage, pageCount - 1);
235
+
236
+ // Every dial-eligible icon, with the page/slot it occupies. The ranking is
237
+ // snapshotted on open, so each icon's page and slot are fixed for the
238
+ // session — which lets us mount all icons once and paginate purely by
239
+ // toggling visibility (no remounts on page change).
240
+ const allDialIcons = (0, _react.useMemo)(() => {
241
+ return rankedIds.map(id => iconsById.get(id)).filter(icon => Boolean(icon)).map((icon, i) => ({
242
+ icon,
243
+ page: Math.floor(i / _floatingToolsCore.MAX_DIAL_SLOTS),
244
+ slot: i % _floatingToolsCore.MAX_DIAL_SLOTS
245
+ }));
246
+ }, [rankedIds, iconsById]);
247
+
248
+ // Empty slot indices for the current page (only the last page can be
249
+ // partial). These are cheap placeholder dots.
250
+ const emptySlots = (0, _react.useMemo)(() => {
251
+ const onThisPage = allDialIcons.filter(d => d.page === safePage).length;
252
+ const slots = [];
253
+ for (let s = onThisPage; s < _floatingToolsCore.MAX_DIAL_SLOTS; s += 1) slots.push(s);
254
+ return slots;
255
+ }, [allDialIcons, safePage]);
256
+
257
+ // Swap to another page. Every icon is already mounted, so this only toggles
258
+ // which ones are visible — no remount, no re-animation — keeping page
259
+ // changes instant.
260
+ const handlePageChange = next => {
261
+ const clamped = Math.max(0, Math.min(next, pageCount - 1));
262
+ if (clamped !== safePage) setCurrentPage(clamped);
263
+ };
232
264
 
233
265
  // Initialize animations on mount - using shared config from core
234
266
  (0, _react.useEffect)(() => {
@@ -249,13 +281,13 @@ const DialDevTools = ({
249
281
  }
250
282
  },
251
283
  centerButton: {
252
- delay: 300,
284
+ delay: 150,
253
285
  damping: 10,
254
286
  stiffness: 200
255
287
  },
256
288
  icons: {
257
- delay: 500,
258
- duration: 600
289
+ delay: 200,
290
+ duration: 400
259
291
  },
260
292
  circuitTraces: {
261
293
  delay: 600,
@@ -326,81 +358,21 @@ const DialDevTools = ({
326
358
  useNativeDriver: true
327
359
  })]).start();
328
360
 
329
- // Subtle glitch effect - using shared config
330
- const glitchAnimation = () => {
331
- _reactNative.Animated.sequence([_reactNative.Animated.timing(glitchOffset, {
332
- toValue: continuous.glitch.offset,
333
- duration: continuous.glitch.stepDuration,
334
- useNativeDriver: true
335
- }), _reactNative.Animated.timing(glitchOffset, {
336
- toValue: -continuous.glitch.offset,
337
- duration: continuous.glitch.stepDuration,
338
- useNativeDriver: true
339
- }), _reactNative.Animated.timing(glitchOffset, {
340
- toValue: 0,
341
- duration: continuous.glitch.stepDuration,
342
- useNativeDriver: true
343
- })]).start();
344
- };
345
- glitchIntervalRef.current = setInterval(glitchAnimation, continuous.glitch.interval);
346
-
347
- // Pulse animation - using shared config
348
- const startPulse = () => {
349
- pulseAnimationRef.current = _reactNative.Animated.loop(_reactNative.Animated.sequence([_reactNative.Animated.timing(pulseScale, {
350
- toValue: continuous.pulse.maxScale,
351
- duration: continuous.pulse.duration,
352
- easing: _reactNative.Easing.inOut(_reactNative.Easing.ease),
353
- useNativeDriver: true
354
- }), _reactNative.Animated.timing(pulseScale, {
355
- toValue: continuous.pulse.minScale,
356
- duration: continuous.pulse.duration,
357
- easing: _reactNative.Easing.inOut(_reactNative.Easing.ease),
358
- useNativeDriver: true
359
- })]));
360
- pulseAnimationRef.current.start();
361
- };
362
- startPulse();
363
-
364
- // Subtle floating animation for the dial - using shared config
365
- _reactNative.Animated.loop(_reactNative.Animated.sequence([_reactNative.Animated.timing(floatingAnim, {
366
- toValue: continuous.floating.maxY,
367
- duration: continuous.floating.duration,
368
- easing: _reactNative.Easing.inOut(_reactNative.Easing.ease),
369
- useNativeDriver: true
370
- }), _reactNative.Animated.timing(floatingAnim, {
371
- toValue: continuous.floating.minY,
372
- duration: continuous.floating.duration,
373
- easing: _reactNative.Easing.inOut(_reactNative.Easing.ease),
374
- useNativeDriver: true
375
- })])).start();
376
-
377
- // Gentle breathing effect for center button - using shared config
378
- _reactNative.Animated.loop(_reactNative.Animated.sequence([_reactNative.Animated.timing(breathingScale, {
379
- toValue: continuous.breathing.maxScale,
380
- duration: continuous.breathing.duration,
361
+ // Pulse animation - only continuous effect kept for performance
362
+ pulseAnimationRef.current = _reactNative.Animated.loop(_reactNative.Animated.sequence([_reactNative.Animated.timing(pulseScale, {
363
+ toValue: continuous.pulse.maxScale,
364
+ duration: continuous.pulse.duration,
381
365
  easing: _reactNative.Easing.inOut(_reactNative.Easing.ease),
382
366
  useNativeDriver: true
383
- }), _reactNative.Animated.timing(breathingScale, {
384
- toValue: continuous.breathing.minScale,
385
- duration: continuous.breathing.duration,
367
+ }), _reactNative.Animated.timing(pulseScale, {
368
+ toValue: continuous.pulse.minScale,
369
+ duration: continuous.pulse.duration,
386
370
  easing: _reactNative.Easing.inOut(_reactNative.Easing.ease),
387
371
  useNativeDriver: true
388
- })])).start();
389
-
390
- // Circuit traces fade in - using shared config
391
- _reactNative.Animated.timing(circuitOpacity, {
392
- toValue: 1,
393
- duration: entrance.circuitTraces.duration,
394
- delay: entrance.circuitTraces.delay,
395
- useNativeDriver: true
396
- }).start();
372
+ })]));
373
+ pulseAnimationRef.current.start();
397
374
  return () => {
398
- if (glitchIntervalRef.current) {
399
- clearInterval(glitchIntervalRef.current);
400
- }
401
- if (pulseAnimationRef.current) {
402
- pulseAnimationRef.current.stop();
403
- }
375
+ pulseAnimationRef.current?.stop();
404
376
  };
405
377
  }, []);
406
378
  const handleOnboardingDismiss = () => {
@@ -472,8 +444,8 @@ const DialDevTools = ({
472
444
  }
473
445
  });
474
446
  };
475
- const handleIconPress = index => {
476
- setSelectedIcon(index);
447
+ const handleIconPress = icon => {
448
+ setSelectedIcon(1);
477
449
  const interaction = _floatingToolsCore.dialAnimationConfig?.interaction ?? {
478
450
  iconSelect: {
479
451
  pulse: [{
@@ -505,9 +477,9 @@ const DialDevTools = ({
505
477
 
506
478
  // Trigger action - using shared delay
507
479
  setTimeout(() => {
508
- icons[index].onPress();
480
+ icon.onPress();
509
481
  // Only close if it's not the WiFi toggle (by id)
510
- if (icons[index].id !== "wifi") {
482
+ if (icon.id !== "wifi") {
511
483
  handleClose();
512
484
  }
513
485
  }, interaction.iconSelect.actionDelay);
@@ -517,14 +489,9 @@ const DialDevTools = ({
517
489
  const backdropAnimatedStyle = {
518
490
  opacity: backdropOpacity
519
491
  };
520
- const glitchAnimatedStyle = {
521
- transform: [{
522
- translateX: glitchOffset
523
- }]
524
- };
525
492
  const centerButtonAnimatedStyle = {
526
493
  transform: [{
527
- scale: _reactNative.Animated.multiply(centerButtonScale, breathingScale)
494
+ scale: centerButtonScale
528
495
  }]
529
496
  };
530
497
  const pulseAnimatedStyle = {
@@ -542,13 +509,11 @@ const DialDevTools = ({
542
509
  onPress: handleClose
543
510
  })
544
511
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Animated.View, {
545
- style: [styles.parent, {
512
+ style: [styles.parent, sizeStyles.parent, {
546
513
  position: "absolute",
547
- left: (SCREEN_WIDTH - CIRCLE_SIZE) / 2,
514
+ left: (screenWidth - circleSize) / 2,
548
515
  bottom: 80,
549
516
  transform: [{
550
- translateY: floatingAnim
551
- }, {
552
517
  scale: dialScale
553
518
  }, {
554
519
  rotate: dialRotation.interpolate({
@@ -558,35 +523,47 @@ const DialDevTools = ({
558
523
  }]
559
524
  }],
560
525
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Animated.View, {
561
- style: [styles.circle, glitchAnimatedStyle],
526
+ style: [styles.circle, sizeStyles.circle],
562
527
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
563
- style: styles.gradientBackground,
528
+ style: [styles.gradientBackground, sizeStyles.rounded],
564
529
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
565
- style: styles.gradientLayer1
530
+ style: [styles.gradientLayer1, sizeStyles.rounded]
566
531
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
567
- style: styles.gradientLayer2
532
+ style: [styles.gradientLayer2, sizeStyles.rounded]
568
533
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
569
- style: styles.gradientLayer3
534
+ style: [styles.gradientLayer3, sizeStyles.rounded]
570
535
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
571
536
  style: styles.gridPattern,
572
537
  children: Array.from({
573
538
  length: 6
574
539
  }).map((_, i) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
575
- style: [styles.gridLine, {
540
+ style: [styles.gridLine, sizeStyles.gridLine, {
576
541
  transform: [{
577
542
  rotate: `${i * 60}deg`
578
543
  }]
579
544
  }]
580
545
  }, i))
581
546
  })]
582
- }), icons.map((icon, i) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_DialIcon.DialIcon, {
583
- selectedIcon: selectedIcon,
547
+ }), allDialIcons.filter(({
548
+ page
549
+ }) => page === safePage).map(({
550
+ icon,
551
+ slot
552
+ }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_DialIcon.DialIcon, {
584
553
  onPress: handleIconPress,
585
554
  iconsProgress: iconsProgress,
586
555
  icon: icon,
587
- index: i,
588
- totalIcons: icons.length
589
- }, `${i}-${icon.name}`))]
556
+ index: slot,
557
+ totalIcons: _floatingToolsCore.MAX_DIAL_SLOTS,
558
+ active: true
559
+ }, icon.id ?? `page${safePage}-${slot}`)), emptySlots.map(slot => /*#__PURE__*/(0, _jsxRuntime.jsx)(_DialIcon.DialIcon, {
560
+ onPress: handleIconPress,
561
+ iconsProgress: iconsProgress,
562
+ icon: createEmptySlot(slot),
563
+ index: slot,
564
+ totalIcons: _floatingToolsCore.MAX_DIAL_SLOTS,
565
+ active: true
566
+ }, `empty-${slot}`))]
590
567
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
591
568
  style: [styles.buttonContainer, centerButtonAnimatedStyle],
592
569
  children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
@@ -644,19 +621,32 @@ const DialDevTools = ({
644
621
  })]
645
622
  })
646
623
  })]
647
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_DevToolsSettingsModal.DevToolsSettingsModal, {
648
- visible: isSettingsModalOpen,
624
+ }), pageCount > 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)(_DialPagination.DialPagination, {
625
+ page: safePage,
626
+ pageCount: pageCount,
627
+ onPrev: () => handlePageChange(safePage - 1),
628
+ onNext: () => handlePageChange(safePage + 1),
629
+ animatedStyle: {
630
+ position: "absolute",
631
+ left: (screenWidth - circleSize) / 2,
632
+ // Circle's bottom edge sits at bottom: 80 -> screenHeight - 80
633
+ // from the top. Place the pager 16px below that edge.
634
+ top: screenHeight - 80 + 16,
635
+ width: circleSize,
636
+ opacity: dialScale,
637
+ transform: [{
638
+ scale: dialScale
639
+ }]
640
+ }
641
+ }), isSettingsModalOpen && /*#__PURE__*/(0, _jsxRuntime.jsx)(_DevToolsSettingsModal.DevToolsSettingsModal, {
642
+ visible: true,
649
643
  onClose: () => {
650
644
  setIsSettingsModalOpen(false);
651
645
  refreshSettings(); // Refresh from storage
652
646
  },
653
- onSettingsChange: newSettings => {
654
- // Immediately update local settings for instant feedback
655
- setLocalSettings(newSettings);
656
- },
657
647
  availableApps: availableApps
658
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_OnboardingTooltip.OnboardingTooltip, {
659
- visible: showOnboardingTooltip && !isSettingsModalOpen && !onboardingDismissedRef.current,
648
+ }), showOnboardingTooltip && !isSettingsModalOpen && !onboardingDismissedRef.current && /*#__PURE__*/(0, _jsxRuntime.jsx)(_OnboardingTooltip.OnboardingTooltip, {
649
+ visible: true,
660
650
  onDismiss: handleOnboardingDismiss
661
651
  })]
662
652
  });
@@ -671,16 +661,13 @@ const styles = _reactNative.StyleSheet.create({
671
661
  ..._reactNative.StyleSheet.absoluteFillObject,
672
662
  backgroundColor: _floatingToolsCore.dialColors.dialBackdrop
673
663
  },
664
+ // width/height/borderRadius for the circle pieces come from sizeStyles —
665
+ // they track the live window width.
674
666
  parent: {
675
- width: CIRCLE_SIZE,
676
- height: CIRCLE_SIZE,
677
667
  alignItems: "center",
678
668
  justifyContent: "center"
679
669
  },
680
670
  circle: {
681
- width: CIRCLE_SIZE,
682
- height: CIRCLE_SIZE,
683
- borderRadius: CIRCLE_SIZE / 2,
684
671
  position: "absolute",
685
672
  backgroundColor: "transparent",
686
673
  borderWidth: 1,
@@ -697,7 +684,6 @@ const styles = _reactNative.StyleSheet.create({
697
684
  gradientBackground: {
698
685
  width: "100%",
699
686
  height: "100%",
700
- borderRadius: CIRCLE_SIZE / 2,
701
687
  position: "relative",
702
688
  backgroundColor: _floatingToolsCore.dialColors.dialBackground,
703
689
  overflow: "hidden"
@@ -705,24 +691,21 @@ const styles = _reactNative.StyleSheet.create({
705
691
  gradientLayer1: {
706
692
  ..._reactNative.StyleSheet.absoluteFillObject,
707
693
  backgroundColor: _floatingToolsCore.dialColors.dialGradient1,
708
- opacity: 0.6,
709
- borderRadius: CIRCLE_SIZE / 2
694
+ opacity: 0.6
710
695
  },
711
696
  gradientLayer2: {
712
697
  ..._reactNative.StyleSheet.absoluteFillObject,
713
698
  backgroundColor: _floatingToolsCore.dialColors.dialGradient2,
714
699
  opacity: 0.4,
715
700
  top: "30%",
716
- left: "30%",
717
- borderRadius: CIRCLE_SIZE / 2
701
+ left: "30%"
718
702
  },
719
703
  gradientLayer3: {
720
704
  ..._reactNative.StyleSheet.absoluteFillObject,
721
705
  backgroundColor: _floatingToolsCore.dialColors.dialGradient3,
722
706
  opacity: 0.3,
723
707
  top: "50%",
724
- left: "50%",
725
- borderRadius: CIRCLE_SIZE / 2
708
+ left: "50%"
726
709
  },
727
710
  gridPattern: {
728
711
  ..._reactNative.StyleSheet.absoluteFillObject,
@@ -731,7 +714,6 @@ const styles = _reactNative.StyleSheet.create({
731
714
  },
732
715
  gridLine: {
733
716
  position: "absolute",
734
- width: CIRCLE_SIZE,
735
717
  height: 1,
736
718
  backgroundColor: _floatingToolsCore.dialColors.dialGridLine
737
719
  },