@buoy-gg/core 2.1.15 → 3.0.1

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 (49) hide show
  1. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.js +3 -33
  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 -2
  6. package/lib/commonjs/floatingMenu/dial/DialDevTools.js +166 -196
  7. package/lib/commonjs/floatingMenu/dial/DialDevTools.web.js +82 -7
  8. package/lib/commonjs/floatingMenu/dial/DialIcon.js +66 -59
  9. package/lib/commonjs/floatingMenu/dial/DialPagination.js +170 -0
  10. package/lib/commonjs/floatingMenu/dial/dialUsageStore.js +97 -0
  11. package/lib/module/floatingMenu/DevToolsSettingsModal.js +3 -33
  12. package/lib/module/floatingMenu/DevToolsSettingsModal.web.js +4 -28
  13. package/lib/module/floatingMenu/FloatingDevTools.js +14 -1
  14. package/lib/module/floatingMenu/FloatingDevTools.web.js +19 -9
  15. package/lib/module/floatingMenu/FloatingMenu.js +6 -2
  16. package/lib/module/floatingMenu/dial/DialDevTools.js +166 -196
  17. package/lib/module/floatingMenu/dial/DialDevTools.web.js +82 -7
  18. package/lib/module/floatingMenu/dial/DialIcon.js +67 -60
  19. package/lib/module/floatingMenu/dial/DialPagination.js +165 -0
  20. package/lib/module/floatingMenu/dial/dialUsageStore.js +89 -0
  21. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
  22. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -1
  23. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  24. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
  25. package/lib/typescript/commonjs/floatingMenu/FloatingMenu.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts +0 -2
  27. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -1
  29. package/lib/typescript/commonjs/floatingMenu/dial/DialIcon.d.ts +7 -2
  30. package/lib/typescript/commonjs/floatingMenu/dial/DialIcon.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/floatingMenu/dial/DialPagination.d.ts +22 -0
  32. package/lib/typescript/commonjs/floatingMenu/dial/DialPagination.d.ts.map +1 -0
  33. package/lib/typescript/commonjs/floatingMenu/dial/dialUsageStore.d.ts +34 -0
  34. package/lib/typescript/commonjs/floatingMenu/dial/dialUsageStore.d.ts.map +1 -0
  35. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
  36. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -1
  37. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  38. package/lib/typescript/module/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
  39. package/lib/typescript/module/floatingMenu/FloatingMenu.d.ts.map +1 -1
  40. package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts +0 -2
  41. package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  42. package/lib/typescript/module/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -1
  43. package/lib/typescript/module/floatingMenu/dial/DialIcon.d.ts +7 -2
  44. package/lib/typescript/module/floatingMenu/dial/DialIcon.d.ts.map +1 -1
  45. package/lib/typescript/module/floatingMenu/dial/DialPagination.d.ts +22 -0
  46. package/lib/typescript/module/floatingMenu/dial/DialPagination.d.ts.map +1 -0
  47. package/lib/typescript/module/floatingMenu/dial/dialUsageStore.d.ts +34 -0
  48. package/lib/typescript/module/floatingMenu/dial/dialUsageStore.d.ts.map +1 -0
  49. package/package.json +5 -5
@@ -17,25 +17,15 @@ const layout = (0, _floatingToolsCore.getDialLayout)({
17
17
  screenWidth: SCREEN_WIDTH
18
18
  });
19
19
  const VIEW_SIZE = layout.iconSize;
20
- const CIRCLE_SIZE = layout.circleSize;
21
20
  const CIRCLE_RADIUS = layout.circleRadius;
22
21
  const DialIcon = ({
23
22
  index,
24
23
  icon,
25
24
  iconsProgress,
26
25
  onPress,
27
- selectedIcon,
28
- totalIcons
26
+ totalIcons,
27
+ active
29
28
  }) => {
30
- // Use shared position calculation from core
31
- const iconPosition = (0, _floatingToolsCore.getIconPosition)(index, totalIcons, layout.iconRadius, _floatingToolsCore.DIAL_START_ANGLE);
32
- const {
33
- x: finalX,
34
- y: finalY,
35
- angle
36
- } = iconPosition;
37
- const radius = layout.iconRadius;
38
-
39
29
  // Animation values - using interpolation for better performance
40
30
  const scale = (0, _react.useRef)(new _reactNative.Animated.Value(1)).current;
41
31
 
@@ -74,52 +64,66 @@ const DialIcon = ({
74
64
  }).start();
75
65
  };
76
66
 
77
- // Use shared stagger calculation from core
78
- const staggerInputRange = (0, _floatingToolsCore.getIconStaggerInputRange)(index, totalIcons);
79
-
80
- // Use interpolation for smooth animation that works both directions
81
- const staggeredProgress = iconsProgress.interpolate({
82
- inputRange: staggerInputRange,
83
- outputRange: [0, 0, 1, 1],
84
- extrapolate: "clamp"
85
- });
67
+ // Position + spiral-entrance interpolations depend only on the (fixed) slot
68
+ // index, so compute them once. This keeps re-renders — which happen on every
69
+ // page change as `active` toggles — cheap.
70
+ const motion = (0, _react.useMemo)(() => {
71
+ const iconPosition = (0, _floatingToolsCore.getIconPosition)(index, totalIcons, layout.iconRadius, _floatingToolsCore.DIAL_START_ANGLE);
72
+ const {
73
+ x: finalX,
74
+ y: finalY,
75
+ angle
76
+ } = iconPosition;
77
+ const radius = layout.iconRadius;
78
+ const staggerInputRange = (0, _floatingToolsCore.getIconStaggerInputRange)(index, totalIcons);
86
79
 
87
- // Spiral animation with interpolation
88
- const spiralRotation = staggeredProgress.interpolate({
89
- inputRange: [0, 1],
90
- outputRange: [Math.PI * 2, 0] // Spiral from 2π to 0
91
- });
80
+ // Use interpolation for smooth animation that works both directions
81
+ const staggeredProgress = iconsProgress.interpolate({
82
+ inputRange: staggerInputRange,
83
+ outputRange: [0, 0, 1, 1],
84
+ extrapolate: "clamp"
85
+ });
92
86
 
93
- // Distance from center
94
- const distance = staggeredProgress.interpolate({
95
- inputRange: [0, 1],
96
- outputRange: [0, radius]
97
- });
87
+ // Spiral animation with interpolation
88
+ const spiralRotation = staggeredProgress.interpolate({
89
+ inputRange: [0, 1],
90
+ outputRange: [Math.PI * 2, 0] // Spiral from 2π to 0
91
+ });
98
92
 
99
- // Calculate X and Y positions using Animated operations
100
- const translateX = _reactNative.Animated.add(_reactNative.Animated.multiply(distance, spiralRotation.interpolate({
101
- inputRange: [0, Math.PI * 2],
102
- outputRange: [Math.cos(angle), Math.cos(angle + Math.PI * 2)]
103
- })), staggeredProgress.interpolate({
104
- inputRange: [0, 1],
105
- outputRange: [0, finalX - radius * Math.cos(angle + Math.PI * 2)]
106
- }));
107
- const translateY = _reactNative.Animated.add(_reactNative.Animated.multiply(distance, spiralRotation.interpolate({
108
- inputRange: [0, Math.PI * 2],
109
- outputRange: [Math.sin(angle), Math.sin(angle + Math.PI * 2)]
110
- })), staggeredProgress.interpolate({
111
- inputRange: [0, 1],
112
- outputRange: [0, finalY - radius * Math.sin(angle + Math.PI * 2)]
113
- }));
93
+ // Distance from center
94
+ const distance = staggeredProgress.interpolate({
95
+ inputRange: [0, 1],
96
+ outputRange: [0, radius]
97
+ });
114
98
 
115
- // Opacity animation
116
- const itemOpacity = staggeredProgress.interpolate({
117
- inputRange: [0, 0.3, 1],
118
- outputRange: [0, 0.3, 1]
119
- });
99
+ // Calculate X and Y positions using Animated operations
100
+ const translateX = _reactNative.Animated.add(_reactNative.Animated.multiply(distance, spiralRotation.interpolate({
101
+ inputRange: [0, Math.PI * 2],
102
+ outputRange: [Math.cos(angle), Math.cos(angle + Math.PI * 2)]
103
+ })), staggeredProgress.interpolate({
104
+ inputRange: [0, 1],
105
+ outputRange: [0, finalX - radius * Math.cos(angle + Math.PI * 2)]
106
+ }));
107
+ const translateY = _reactNative.Animated.add(_reactNative.Animated.multiply(distance, spiralRotation.interpolate({
108
+ inputRange: [0, Math.PI * 2],
109
+ outputRange: [Math.sin(angle), Math.sin(angle + Math.PI * 2)]
110
+ })), staggeredProgress.interpolate({
111
+ inputRange: [0, 1],
112
+ outputRange: [0, finalY - radius * Math.sin(angle + Math.PI * 2)]
113
+ }));
120
114
 
121
- // Scale based on progress
122
- const progressScale = staggeredProgress;
115
+ // Opacity animation
116
+ const itemOpacity = staggeredProgress.interpolate({
117
+ inputRange: [0, 0.3, 1],
118
+ outputRange: [0, 0.3, 1]
119
+ });
120
+ return {
121
+ translateX,
122
+ translateY,
123
+ itemOpacity,
124
+ progressScale: staggeredProgress
125
+ };
126
+ }, [index, totalIcons, iconsProgress]);
123
127
 
124
128
  // Main animated style for position and appearance
125
129
  const animatedStyle = {
@@ -128,24 +132,24 @@ const DialIcon = ({
128
132
  // Center position
129
133
  top: CIRCLE_RADIUS - VIEW_SIZE / 2,
130
134
  // Center position
131
- opacity: itemOpacity,
135
+ opacity: motion.itemOpacity,
132
136
  transform: [{
133
- translateX
137
+ translateX: motion.translateX
134
138
  },
135
139
  // Apply translation from center
136
140
  {
137
- translateY
141
+ translateY: motion.translateY
138
142
  },
139
143
  // Apply translation from center
140
144
  {
141
- scale: _reactNative.Animated.multiply(scale, progressScale)
145
+ scale: _reactNative.Animated.multiply(scale, motion.progressScale)
142
146
  }]
143
147
  };
144
148
 
145
149
  // Check if this is an empty spot (no icon and no iconComponent)
146
150
  const isEmpty = icon.icon === null && !icon.iconComponent;
147
151
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
148
- style: [styles.view, animatedStyle],
152
+ style: [styles.view, animatedStyle, !active && styles.hidden],
149
153
  children: isEmpty ?
150
154
  /*#__PURE__*/
151
155
  // Empty spot - just show a subtle circle
@@ -155,7 +159,7 @@ const DialIcon = ({
155
159
  style: styles.emptyDot
156
160
  })
157
161
  }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
158
- onPress: () => onPress(index),
162
+ onPress: () => onPress(icon),
159
163
  onPressIn: handlePressIn,
160
164
  onPressOut: handlePressOut,
161
165
  style: styles.pressable,
@@ -194,6 +198,9 @@ const styles = _reactNative.StyleSheet.create({
194
198
  justifyContent: "center",
195
199
  alignItems: "center"
196
200
  },
201
+ hidden: {
202
+ display: "none"
203
+ },
197
204
  pressable: {
198
205
  width: "100%",
199
206
  height: "100%",
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.DialPagination = void 0;
7
+ var _react = require("react");
8
+ var _reactNative = require("react-native");
9
+ var _sharedUi = require("@buoy-gg/shared-ui");
10
+ var _floatingToolsCore = require("@buoy-gg/floating-tools-core");
11
+ var _jsxRuntime = require("react/jsx-runtime");
12
+ /** Pad a 1-based page number to two digits, e.g. 3 -> "03". */
13
+ const pad = n => String(n).padStart(2, "0");
14
+ /**
15
+ * A single pager button. Press feedback is driven by a native-driver
16
+ * `Animated` value via `onPressIn`/`onPressOut` — so the scale reacts
17
+ * instantly on the UI thread, with no JS re-render in the press path.
18
+ */
19
+ const PagerButton = ({
20
+ side,
21
+ label,
22
+ disabled,
23
+ onPress
24
+ }) => {
25
+ const scale = (0, _react.useRef)(new _reactNative.Animated.Value(1)).current;
26
+ const springTo = toValue => {
27
+ _reactNative.Animated.spring(scale, {
28
+ toValue,
29
+ damping: 15,
30
+ stiffness: 400,
31
+ useNativeDriver: true
32
+ }).start();
33
+ };
34
+ const accent = disabled ? _floatingToolsCore.dialColors.emptyDotBorder : _sharedUi.buoyColors.primary;
35
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
36
+ style: {
37
+ transform: [{
38
+ scale
39
+ }]
40
+ },
41
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
42
+ accessibilityRole: "button",
43
+ accessibilityLabel: `${side === "prev" ? "Previous" : "Next"} dial page`,
44
+ accessibilityState: {
45
+ disabled
46
+ },
47
+ disabled: disabled,
48
+ onPress: onPress,
49
+ onPressIn: () => !disabled && springTo(0.92),
50
+ onPressOut: () => springTo(1),
51
+ style: [styles.button, disabled && styles.buttonDisabled],
52
+ children: [side === "prev" && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ChevronLeft, {
53
+ size: 18,
54
+ color: accent
55
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
56
+ style: [styles.buttonText, disabled && styles.buttonTextDisabled],
57
+ children: label
58
+ }), side === "next" && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ChevronRight, {
59
+ size: 18,
60
+ color: accent
61
+ })]
62
+ })
63
+ });
64
+ };
65
+
66
+ /**
67
+ * Prev/Next pager shown below the dial when there are more dial tools than
68
+ * fit on a single page. Tools are ranked by usage, so paging walks from the
69
+ * most-used tools toward the least-used.
70
+ */
71
+ const DialPagination = ({
72
+ page,
73
+ pageCount,
74
+ onPrev,
75
+ onNext,
76
+ animatedStyle
77
+ }) => {
78
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Animated.View, {
79
+ style: [styles.container, animatedStyle],
80
+ pointerEvents: "box-none",
81
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(PagerButton, {
82
+ side: "prev",
83
+ label: "PREV",
84
+ disabled: page <= 0,
85
+ onPress: onPrev
86
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
87
+ style: styles.indicator,
88
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
89
+ style: styles.indicatorText,
90
+ children: [pad(page + 1), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
91
+ style: styles.indicatorTextDim,
92
+ children: [" / ", pad(pageCount)]
93
+ })]
94
+ })
95
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(PagerButton, {
96
+ side: "next",
97
+ label: "NEXT",
98
+ disabled: page >= pageCount - 1,
99
+ onPress: onNext
100
+ })]
101
+ });
102
+ };
103
+ exports.DialPagination = DialPagination;
104
+ const styles = _reactNative.StyleSheet.create({
105
+ container: {
106
+ flexDirection: "row",
107
+ alignItems: "center",
108
+ justifyContent: "space-between"
109
+ },
110
+ button: {
111
+ flexDirection: "row",
112
+ alignItems: "center",
113
+ gap: 4,
114
+ paddingHorizontal: 16,
115
+ paddingVertical: 10,
116
+ borderRadius: 10,
117
+ borderWidth: 1,
118
+ borderColor: _floatingToolsCore.dialColors.dialBorder,
119
+ backgroundColor: _floatingToolsCore.dialColors.dialBackground,
120
+ shadowColor: _floatingToolsCore.dialColors.dialShadow,
121
+ shadowOffset: {
122
+ width: 0,
123
+ height: 0
124
+ },
125
+ shadowOpacity: 0.4,
126
+ shadowRadius: 8,
127
+ elevation: 6
128
+ },
129
+ buttonDisabled: {
130
+ opacity: 0.4
131
+ },
132
+ buttonText: {
133
+ color: _sharedUi.buoyColors.primary,
134
+ fontSize: 12,
135
+ fontWeight: "900",
136
+ fontFamily: "monospace",
137
+ letterSpacing: 1.5,
138
+ textShadowColor: _sharedUi.buoyColors.primary,
139
+ textShadowOffset: {
140
+ width: 0,
141
+ height: 0
142
+ },
143
+ textShadowRadius: 4
144
+ },
145
+ buttonTextDisabled: {
146
+ color: _floatingToolsCore.dialColors.emptyDotBorder,
147
+ textShadowRadius: 0
148
+ },
149
+ indicator: {
150
+ paddingHorizontal: 12,
151
+ paddingVertical: 6
152
+ },
153
+ indicatorText: {
154
+ color: "#FFFFFF",
155
+ fontSize: 13,
156
+ fontWeight: "900",
157
+ fontFamily: "monospace",
158
+ letterSpacing: 2,
159
+ textShadowColor: _floatingToolsCore.dialColors.dialShadow,
160
+ textShadowOffset: {
161
+ width: 0,
162
+ height: 0
163
+ },
164
+ textShadowRadius: 6
165
+ },
166
+ indicatorTextDim: {
167
+ color: _floatingToolsCore.dialColors.iconLabel,
168
+ textShadowRadius: 0
169
+ }
170
+ });
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.getRankedToolIds = getRankedToolIds;
7
+ exports.isDialUsageLoaded = isDialUsageLoaded;
8
+ exports.loadDialUsage = loadDialUsage;
9
+ exports.recordToolUsage = recordToolUsage;
10
+ exports.resetDialUsage = resetDialUsage;
11
+ var _sharedUi = require("@buoy-gg/shared-ui");
12
+ var _floatingToolsCore = require("@buoy-gg/floating-tools-core");
13
+ /**
14
+ * Dial Usage Store - persists and ranks dial tool usage.
15
+ *
16
+ * Wraps the pure scoring logic from `@buoy-gg/floating-tools-core` with a
17
+ * persisted, in-memory cache. The dial menu uses this to order tools by how
18
+ * recently/frequently they are used, so the most-used tools land on page 1.
19
+ *
20
+ * The cache is loaded eagerly on import so `getRankedToolIds` can run
21
+ * synchronously by the time the dial opens.
22
+ */
23
+
24
+ const STORAGE_KEY = _sharedUi.devToolsStorageKeys.dial.usage();
25
+ let cache = {};
26
+ let loaded = false;
27
+ let loadPromise = null;
28
+
29
+ /**
30
+ * Load persisted usage data into the in-memory cache. Safe to call multiple
31
+ * times — the underlying read happens only once.
32
+ */
33
+ function loadDialUsage() {
34
+ if (loadPromise) return loadPromise;
35
+ loadPromise = (async () => {
36
+ try {
37
+ const raw = await _sharedUi.persistentStorage.getItem(STORAGE_KEY);
38
+ if (raw) {
39
+ const parsed = JSON.parse(raw);
40
+ if (parsed && typeof parsed === "object") {
41
+ cache = parsed;
42
+ }
43
+ }
44
+ } catch {
45
+ // Ignore — start with an empty usage map.
46
+ } finally {
47
+ loaded = true;
48
+ }
49
+ })();
50
+ return loadPromise;
51
+ }
52
+
53
+ // Kick off the load as soon as this module is imported.
54
+ void loadDialUsage();
55
+
56
+ /** Whether the usage cache has finished loading from storage. */
57
+ function isDialUsageLoaded() {
58
+ return loaded;
59
+ }
60
+
61
+ /**
62
+ * Rank tool ids by recency-weighted usage, highest first. Synchronous —
63
+ * operates against the in-memory cache. Never-used tools keep their original
64
+ * order as a tie-breaker.
65
+ *
66
+ * @param orderedIds - Tool ids in their default/registration order
67
+ */
68
+ function getRankedToolIds(orderedIds) {
69
+ return (0, _floatingToolsCore.rankToolIds)(orderedIds, cache, Date.now());
70
+ }
71
+
72
+ /**
73
+ * Record a single press of a tool and persist the updated usage map.
74
+ *
75
+ * @param id - Tool id that was pressed
76
+ */
77
+ async function recordToolUsage(id) {
78
+ if (!id) return;
79
+ if (!loaded) await loadDialUsage();
80
+ const now = Date.now();
81
+ cache = (0, _floatingToolsCore.pruneUsage)((0, _floatingToolsCore.recordUsage)(cache, id, now), now);
82
+ try {
83
+ await _sharedUi.persistentStorage.setItem(STORAGE_KEY, JSON.stringify(cache));
84
+ } catch {
85
+ // Ignore persistence failure — the in-memory cache is still updated.
86
+ }
87
+ }
88
+
89
+ /** Clear all usage data (in-memory and persisted). */
90
+ async function resetDialUsage() {
91
+ cache = {};
92
+ try {
93
+ await _sharedUi.persistentStorage.removeItem(STORAGE_KEY);
94
+ } catch {
95
+ // Ignore — the in-memory cache is already cleared.
96
+ }
97
+ }
@@ -155,7 +155,7 @@ export const DevToolsSettingsModal = ({
155
155
  const allowedDialKeys = useMemo(() => Object.keys(defaultSettings.dialTools), [defaultSettings]);
156
156
  const allowedFloatingKeys = useMemo(() => Object.keys(defaultSettings.floatingTools).filter(key => key !== "environment"), [defaultSettings]);
157
157
  const [settings, setSettings] = useState(initialSettings || defaultSettings);
158
- const [activeTab, setActiveTab] = useState("dial");
158
+ const [activeTab, setActiveTab] = useState("floating");
159
159
  const [activeTabLoaded, setActiveTabLoaded] = useState(false);
160
160
  const [expandedSettings, setExpandedSettings] = useState(new Set());
161
161
  const [showLicenseModal, setShowLicenseModal] = useState(false);
@@ -165,7 +165,7 @@ export const DevToolsSettingsModal = ({
165
165
  const loadActiveTab = async () => {
166
166
  try {
167
167
  const savedTab = await persistentStorage.getItem(devToolsStorageKeys.settings.activeTab());
168
- if (savedTab && ["dial", "floating", "settings", "pro"].includes(savedTab)) {
168
+ if (savedTab && ["floating", "settings", "pro"].includes(savedTab)) {
169
169
  setActiveTab(savedTab);
170
170
  }
171
171
  } catch (error) {
@@ -263,23 +263,6 @@ export const DevToolsSettingsModal = ({
263
263
  console.error("Failed to save dev tools settings:", error);
264
264
  }
265
265
  };
266
- const toggleDialTool = tool => {
267
- const currentEnabled = Object.values(settings.dialTools).filter(v => v).length;
268
- const isCurrentlyEnabled = settings.dialTools[tool];
269
-
270
- // If trying to enable and already at 6, don't allow
271
- if (!isCurrentlyEnabled && currentEnabled >= MAX_DIAL_SLOTS) {
272
- return; // Could also show a toast/alert here
273
- }
274
- const newSettings = {
275
- ...settings,
276
- dialTools: {
277
- ...settings.dialTools,
278
- [tool]: !settings.dialTools[tool]
279
- }
280
- };
281
- saveSettings(newSettings);
282
- };
283
266
  const toggleFloatingTool = tool => {
284
267
  const newSettings = {
285
268
  ...settings,
@@ -647,17 +630,7 @@ export const DevToolsSettingsModal = ({
647
630
  style: styles.scrollContent,
648
631
  showsVerticalScrollIndicator: false,
649
632
  contentContainerStyle: styles.scrollContainer,
650
- children: [activeTab === "dial" && /*#__PURE__*/_jsx(View, {
651
- style: styles.section,
652
- children: (() => {
653
- const enabledCount = Object.values(settings.dialTools).filter(v => v).length;
654
- const isAtLimit = enabledCount >= MAX_DIAL_SLOTS;
655
- return Object.entries(settings.dialTools).map(([key, value]) => {
656
- const isDisabled = !value && isAtLimit;
657
- return renderToolCard(key, value, isDisabled, () => toggleDialTool(key));
658
- });
659
- })()
660
- }), activeTab === "floating" && /*#__PURE__*/_jsx(View, {
633
+ children: [activeTab === "floating" && /*#__PURE__*/_jsx(View, {
661
634
  style: styles.section,
662
635
  children: Object.entries(settings.floatingTools).map(([key, value]) => renderToolCard(key, value, false, () => toggleFloatingTool(key)))
663
636
  }), activeTab === "settings" && /*#__PURE__*/_jsxs(View, {
@@ -1112,9 +1085,6 @@ export const DevToolsSettingsModal = ({
1112
1085
  noMargin: true,
1113
1086
  children: /*#__PURE__*/_jsx(TabSelector, {
1114
1087
  tabs: [{
1115
- key: "dial",
1116
- label: "DIAL"
1117
- }, {
1118
1088
  key: "floating",
1119
1089
  label: "FLOATING"
1120
1090
  }, {
@@ -23,9 +23,7 @@ settingsColors, settingsStyles, getToolColor,
23
23
  // Tool metadata
24
24
  getToolLabel, getToolDescription,
25
25
  // Global settings config
26
- globalSettingsConfig,
27
- // Constants
28
- MAX_SETTINGS_DIAL_SLOTS
26
+ globalSettingsConfig
29
27
  // Types
30
28
  } from '@buoy-gg/floating-tools-react';
31
29
  import { ReduxIcon } from '@buoy-gg/floating-tools-core';
@@ -444,15 +442,14 @@ export function SettingsModal({
444
442
  storage = webStorageAdapter,
445
443
  onSettingsChange
446
444
  }) {
447
- const [activeTab, setActiveTab] = useState('dial');
445
+ const [activeTab, setActiveTab] = useState('floating');
448
446
  const [isExiting, setIsExiting] = useState(false);
449
447
 
450
448
  // Use the shared settings hook
451
449
  const {
452
450
  settings,
453
451
  isLoading,
454
- actions,
455
- helpers
452
+ actions
456
453
  } = useSettings({
457
454
  availableTools,
458
455
  defaultFloatingTools,
@@ -605,28 +602,7 @@ export function SettingsModal({
605
602
  },
606
603
  children: "Loading settings..."
607
604
  }) : /*#__PURE__*/_jsxs(_Fragment, {
608
- children: [activeTab === 'dial' && /*#__PURE__*/_jsxs("div", {
609
- children: [/*#__PURE__*/_jsxs("div", {
610
- style: {
611
- fontSize: settingsStyles.sectionTitle.fontSize,
612
- fontWeight: settingsStyles.sectionTitle.fontWeight,
613
- letterSpacing: settingsStyles.sectionTitle.letterSpacing,
614
- textTransform: settingsStyles.sectionTitle.textTransform,
615
- color: settingsColors.textMuted,
616
- marginBottom: settingsStyles.sectionHeader.marginBottom
617
- },
618
- children: ["SELECT UP TO ", MAX_SETTINGS_DIAL_SLOTS, " TOOLS (", helpers.dialToolCount, "/", MAX_SETTINGS_DIAL_SLOTS, ")"]
619
- }), Object.entries(settings.dialTools).map(([id, enabled]) => {
620
- const isDisabled = !enabled && helpers.isDialFull;
621
- return /*#__PURE__*/_jsx(ToolCard, {
622
- toolId: id,
623
- enabled: enabled,
624
- disabled: isDisabled,
625
- availableTools: availableTools,
626
- onToggle: () => actions.toggleDialTool(id)
627
- }, id);
628
- })]
629
- }), activeTab === 'floating' && /*#__PURE__*/_jsxs("div", {
605
+ children: [activeTab === 'floating' && /*#__PURE__*/_jsxs("div", {
630
606
  children: [/*#__PURE__*/_jsx("div", {
631
607
  style: {
632
608
  fontSize: settingsStyles.sectionTitle.fontSize,
@@ -273,6 +273,19 @@ export const FloatingDevTools = ({
273
273
  }
274
274
  }, []);
275
275
 
276
+ // Check if impersonate is installed and auto-render the floating banner
277
+ const ImpersonateOverlay = useMemo(() => {
278
+ try {
279
+ // @ts-ignore - Dynamic import that may not exist
280
+ const {
281
+ ImpersonateOverlay: Overlay
282
+ } = require("@buoy-gg/impersonate");
283
+ return Overlay;
284
+ } catch {
285
+ return null;
286
+ }
287
+ }, []);
288
+
276
289
  // Get tool icon helper for the MinimizedToolsProvider
277
290
  const getToolIcon = useCallback(id => {
278
291
  const tool = finalApps.find(app => app.id === id);
@@ -325,7 +338,7 @@ export const FloatingDevTools = ({
325
338
  environment: resolvedEnvironment
326
339
  }), /*#__PURE__*/_jsx(AppOverlay, {})]
327
340
  })
328
- }), children, DebugBordersOverlay && /*#__PURE__*/_jsx(DebugBordersOverlay, {}), HighlightUpdatesOverlay && /*#__PURE__*/_jsx(HighlightUpdatesOverlay, {}), ImageOverlayOverlay && /*#__PURE__*/_jsx(ImageOverlayOverlay, {}), PerfMonitorOverlay && /*#__PURE__*/_jsx(PerfMonitorOverlay, {}), RouteTracker && /*#__PURE__*/_jsx(RouteTracker, {})]
341
+ }), children, DebugBordersOverlay && /*#__PURE__*/_jsx(DebugBordersOverlay, {}), HighlightUpdatesOverlay && /*#__PURE__*/_jsx(HighlightUpdatesOverlay, {}), ImageOverlayOverlay && /*#__PURE__*/_jsx(ImageOverlayOverlay, {}), PerfMonitorOverlay && /*#__PURE__*/_jsx(PerfMonitorOverlay, {}), ImpersonateOverlay && /*#__PURE__*/_jsx(ImpersonateOverlay, {}), RouteTracker && /*#__PURE__*/_jsx(RouteTracker, {})]
329
342
  })
330
343
  })
331
344
  })
@@ -24,6 +24,7 @@ import { useSettings, getToolLabel, getToolDescription, getToolColor } from '@bu
24
24
  import { autoDiscoverWithCustom, getToolsBySlot } from "../utils/autoDiscoverPresets.web.js";
25
25
  import { FloatingTools, UserStatus } from "./floatingTools.web.js";
26
26
  import { DialMenu } from "./dial/DialDevTools.web.js";
27
+ import { getRankedToolIds, recordToolUsage } from "./dial/dialUsageStore.js";
27
28
  import { SettingsModal } from "./DevToolsSettingsModal.web.js";
28
29
 
29
30
  // =============================
@@ -69,18 +70,17 @@ export function FloatingDevTools({
69
70
  slot: tool.slot
70
71
  })), [tools]);
71
72
 
72
- // Use shared settings hook
73
- const {
74
- settings
75
- } = useSettings({
73
+ // Register available tools with the shared settings hook (used by the
74
+ // settings modal). The dial no longer reads per-tool show/hide state.
75
+ useSettings({
76
76
  availableTools: availableToolsConfig
77
77
  });
78
78
 
79
- // Build dial icons from enabled dial tools
79
+ // Build dial icons for every dial-eligible tool. The dial paginates and
80
+ // ranks them by usage, so there is no per-tool show/hide setting.
80
81
  const dialIcons = useMemo(() => {
81
82
  const dialTools = getToolsBySlot(tools, 'dial');
82
- const enabledDialIds = Object.entries(settings.dialTools).filter(([_, enabled]) => enabled).map(([id]) => id);
83
- return dialTools.filter(tool => enabledDialIds.includes(tool.id)).map(tool => {
83
+ return dialTools.map(tool => {
84
84
  const color = tool.color || getToolColor(tool.id);
85
85
  // Handle icon - can be string (emoji) or function (size) => ReactNode
86
86
  let icon;
@@ -98,12 +98,22 @@ export function FloatingDevTools({
98
98
  icon,
99
99
  color,
100
100
  onPress: () => {
101
+ // Record usage so frequently/recently used tools rank toward page 1.
102
+ void recordToolUsage(tool.id);
101
103
  tool.onPress?.();
102
104
  setIsDialOpen(false);
103
105
  }
104
106
  };
105
107
  });
106
- }, [tools, settings.dialTools]);
108
+ }, [tools]);
109
+
110
+ // Snapshot the usage-ranked order each time the dial opens. It stays stable
111
+ // while open so icons don't jump positions mid-interaction.
112
+ const rankedDialIcons = useMemo(() => {
113
+ if (!isDialOpen) return dialIcons;
114
+ const byId = new Map(dialIcons.map(i => [i.id, i]));
115
+ return getRankedToolIds(dialIcons.map(i => i.id)).map(id => byId.get(id)).filter(i => Boolean(i));
116
+ }, [isDialOpen, dialIcons]);
107
117
 
108
118
  // Build available tools for settings modal
109
119
  const availableTools = useMemo(() => tools.map(tool => ({
@@ -129,7 +139,7 @@ export function FloatingDevTools({
129
139
  onPress: handleOpenDial
130
140
  })
131
141
  }), isDialOpen && /*#__PURE__*/_jsx(DialMenu, {
132
- icons: dialIcons,
142
+ icons: rankedDialIcons,
133
143
  onClose: handleCloseDial,
134
144
  centerLabel: "BUOY",
135
145
  onCenterPress: handleOpenSettings