@buoy-gg/core 2.1.15 → 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
|
@@ -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
|
-
|
|
28
|
-
|
|
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
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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(
|
|
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("
|
|
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 && ["
|
|
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 === "
|
|
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('
|
|
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 === '
|
|
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
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
142
|
+
icons: rankedDialIcons,
|
|
133
143
|
onClose: handleCloseDial,
|
|
134
144
|
centerLabel: "BUOY",
|
|
135
145
|
onCenterPress: handleOpenSettings
|