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