@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
@@ -214,8 +214,14 @@ function DialMenu({
214
214
  }), []);
215
215
  const gridRotations = (0, _react.useMemo)(() => (0, _floatingToolsReact.getGridLineRotations)(), []);
216
216
  const positions = (0, _react.useMemo)(() => (0, _floatingToolsReact.getAllIconPositions)(_floatingToolsReact.MAX_DIAL_SLOTS, layout.iconRadius), [layout.iconRadius]);
217
+
218
+ // Pagination: tools are split across pages of MAX_DIAL_SLOTS.
219
+ const [currentPage, setCurrentPage] = (0, _react.useState)(0);
220
+ const pageCount = Math.max(1, Math.ceil(icons.length / _floatingToolsReact.MAX_DIAL_SLOTS));
221
+ const safePage = Math.min(currentPage, pageCount - 1);
217
222
  const paddedIcons = (0, _react.useMemo)(() => {
218
- const result = [...icons.slice(0, _floatingToolsReact.MAX_DIAL_SLOTS)];
223
+ const start = safePage * _floatingToolsReact.MAX_DIAL_SLOTS;
224
+ const result = [...icons.slice(start, start + _floatingToolsReact.MAX_DIAL_SLOTS)];
219
225
  while (result.length < _floatingToolsReact.MAX_DIAL_SLOTS) {
220
226
  result.push({
221
227
  id: `empty-${result.length}`,
@@ -225,7 +231,7 @@ function DialMenu({
225
231
  });
226
232
  }
227
233
  return result;
228
- }, [icons]);
234
+ }, [icons, safePage]);
229
235
 
230
236
  // Inject keyframes
231
237
  (0, _react.useEffect)(() => {
@@ -402,10 +408,37 @@ function DialMenu({
402
408
  }, interaction.iconSelect.actionDelay);
403
409
  }, [interaction.iconSelect, handleClose]);
404
410
 
411
+ // Page navigation - icons are keyed by slot index, so this only swaps
412
+ // their content in place (no remount, no re-animation) for an instant page
413
+ // change.
414
+ const handlePageChange = (0, _react.useCallback)(next => {
415
+ if (isClosingRef.current) return;
416
+ const clamped = Math.max(0, Math.min(next, pageCount - 1));
417
+ if (clamped !== safePage) setCurrentPage(clamped);
418
+ }, [pageCount, safePage]);
419
+
405
420
  // Computed values
406
421
  const buttonContainerSize = layout.buttonSize * _floatingToolsReact.dialStyles.centerButton.containerRatio;
407
422
  const buttonBorderSize = layout.buttonSize * _floatingToolsReact.dialStyles.centerButton.borderRatio;
408
423
  const isAnimating = entranceComplete && !isExiting;
424
+ const pagerButtonStyle = disabled => ({
425
+ display: 'flex',
426
+ alignItems: 'center',
427
+ gap: 4,
428
+ padding: '8px 16px',
429
+ borderRadius: 10,
430
+ border: `1px solid ${_floatingToolsReact.dialColors.dialBorder}`,
431
+ backgroundColor: _floatingToolsReact.dialColors.dialBackground,
432
+ color: disabled ? _floatingToolsReact.dialColors.emptyDotBorder : _floatingToolsReact.dialColors.dialShadow,
433
+ fontSize: 12,
434
+ fontWeight: 900,
435
+ fontFamily: 'monospace',
436
+ letterSpacing: 1.5,
437
+ textTransform: 'uppercase',
438
+ cursor: disabled ? 'default' : 'pointer',
439
+ opacity: disabled ? 0.4 : 1,
440
+ boxShadow: disabled ? 'none' : `0 0 8px ${_floatingToolsReact.dialColors.dialShadow}66`
441
+ });
409
442
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
410
443
  role: "dialog",
411
444
  "aria-label": "Dial Menu",
@@ -430,11 +463,15 @@ function DialMenu({
430
463
  backgroundColor: _floatingToolsReact.dialColors.dialBackdrop,
431
464
  opacity: backdropOpacity
432
465
  }
433
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
466
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
434
467
  style: {
435
- animation: isAnimating ? cssAnimations.floating : 'none'
468
+ animation: isAnimating ? cssAnimations.floating : 'none',
469
+ display: 'flex',
470
+ flexDirection: 'column',
471
+ alignItems: 'center',
472
+ gap: 16
436
473
  },
437
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
474
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
438
475
  style: {
439
476
  animation: triggerGlitch > 0 && isAnimating ? cssAnimations.glitch : 'none'
440
477
  },
@@ -497,7 +534,7 @@ function DialMenu({
497
534
  position: positions[index],
498
535
  progress: iconProgress,
499
536
  onPress: () => handleIconPress(icon)
500
- }, icon.id)), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
537
+ }, index)), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
501
538
  style: {
502
539
  position: 'absolute',
503
540
  left: '50%',
@@ -587,7 +624,45 @@ function DialMenu({
587
624
  })]
588
625
  })]
589
626
  })
590
- }, triggerGlitch)
627
+ }, triggerGlitch), pageCount > 1 && /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
628
+ style: {
629
+ display: 'flex',
630
+ alignItems: 'center',
631
+ gap: 12,
632
+ opacity: Math.min(1, dialScale)
633
+ },
634
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("button", {
635
+ type: "button",
636
+ "aria-label": "Previous dial page",
637
+ disabled: safePage <= 0,
638
+ onClick: () => handlePageChange(safePage - 1),
639
+ style: pagerButtonStyle(safePage <= 0),
640
+ children: "\u2039 PREV"
641
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("span", {
642
+ style: {
643
+ fontSize: 13,
644
+ fontWeight: 900,
645
+ fontFamily: 'monospace',
646
+ letterSpacing: 2,
647
+ color: '#FFFFFF',
648
+ textShadow: `0 0 6px ${_floatingToolsReact.dialColors.dialShadow}`
649
+ },
650
+ children: [String(safePage + 1).padStart(2, '0'), /*#__PURE__*/(0, _jsxRuntime.jsxs)("span", {
651
+ style: {
652
+ color: _floatingToolsReact.dialColors.iconLabel,
653
+ textShadow: 'none'
654
+ },
655
+ children: [' / ', String(pageCount).padStart(2, '0')]
656
+ })]
657
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)("button", {
658
+ type: "button",
659
+ "aria-label": "Next dial page",
660
+ disabled: safePage >= pageCount - 1,
661
+ onClick: () => handlePageChange(safePage + 1),
662
+ style: pagerButtonStyle(safePage >= pageCount - 1),
663
+ children: "NEXT \u203A"
664
+ })]
665
+ })]
591
666
  })]
592
667
  });
593
668
  }
@@ -8,36 +8,25 @@ var _react = require("react");
8
8
  var _reactNative = require("react-native");
9
9
  var _floatingToolsCore = require("@buoy-gg/floating-tools-core");
10
10
  var _jsxRuntime = require("react/jsx-runtime");
11
- const {
12
- width: SCREEN_WIDTH
13
- } = _reactNative.Dimensions.get("window");
14
-
15
- // Use shared layout calculation
16
- const layout = (0, _floatingToolsCore.getDialLayout)({
17
- screenWidth: SCREEN_WIDTH
18
- });
19
- const VIEW_SIZE = layout.iconSize;
20
- const CIRCLE_SIZE = layout.circleSize;
21
- const CIRCLE_RADIUS = layout.circleRadius;
11
+ // The circle radius depends on the live window width and is computed inside
12
+ // the component (must match DialDevTools' circle, which does the same).
13
+ const VIEW_SIZE = _floatingToolsCore.DIAL_ICON_SIZE;
22
14
  const DialIcon = ({
23
15
  index,
24
16
  icon,
25
17
  iconsProgress,
26
18
  onPress,
27
- selectedIcon,
28
- totalIcons
19
+ totalIcons,
20
+ active
29
21
  }) => {
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
22
  // Animation values - using interpolation for better performance
40
23
  const scale = (0, _react.useRef)(new _reactNative.Animated.Value(1)).current;
24
+ const {
25
+ width: screenWidth
26
+ } = (0, _reactNative.useWindowDimensions)();
27
+ const layout = (0, _react.useMemo)(() => (0, _floatingToolsCore.getDialLayout)({
28
+ screenWidth
29
+ }), [screenWidth]);
41
30
 
42
31
  // Hover animation on press in/out - using shared config
43
32
  // Fallback values in case dialAnimationConfig hasn't loaded yet
@@ -74,78 +63,92 @@ const DialIcon = ({
74
63
  }).start();
75
64
  };
76
65
 
77
- // Use shared stagger calculation from core
78
- const staggerInputRange = (0, _floatingToolsCore.getIconStaggerInputRange)(index, totalIcons);
66
+ // Position + spiral-entrance interpolations depend only on the (fixed) slot
67
+ // index, so compute them once. This keeps re-renders — which happen on every
68
+ // page change as `active` toggles — cheap.
69
+ const motion = (0, _react.useMemo)(() => {
70
+ const iconPosition = (0, _floatingToolsCore.getIconPosition)(index, totalIcons, layout.iconRadius, _floatingToolsCore.DIAL_START_ANGLE);
71
+ const {
72
+ x: finalX,
73
+ y: finalY,
74
+ angle
75
+ } = iconPosition;
76
+ const radius = layout.iconRadius;
77
+ const staggerInputRange = (0, _floatingToolsCore.getIconStaggerInputRange)(index, totalIcons);
79
78
 
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
- });
79
+ // Use interpolation for smooth animation that works both directions
80
+ const staggeredProgress = iconsProgress.interpolate({
81
+ inputRange: staggerInputRange,
82
+ outputRange: [0, 0, 1, 1],
83
+ extrapolate: "clamp"
84
+ });
86
85
 
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
- });
86
+ // Spiral animation with interpolation
87
+ const spiralRotation = staggeredProgress.interpolate({
88
+ inputRange: [0, 1],
89
+ outputRange: [Math.PI * 2, 0] // Spiral from 2π to 0
90
+ });
92
91
 
93
- // Distance from center
94
- const distance = staggeredProgress.interpolate({
95
- inputRange: [0, 1],
96
- outputRange: [0, radius]
97
- });
92
+ // Distance from center
93
+ const distance = staggeredProgress.interpolate({
94
+ inputRange: [0, 1],
95
+ outputRange: [0, radius]
96
+ });
98
97
 
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
- }));
98
+ // Calculate X and Y positions using Animated operations
99
+ const translateX = _reactNative.Animated.add(_reactNative.Animated.multiply(distance, spiralRotation.interpolate({
100
+ inputRange: [0, Math.PI * 2],
101
+ outputRange: [Math.cos(angle), Math.cos(angle + Math.PI * 2)]
102
+ })), staggeredProgress.interpolate({
103
+ inputRange: [0, 1],
104
+ outputRange: [0, finalX - radius * Math.cos(angle + Math.PI * 2)]
105
+ }));
106
+ const translateY = _reactNative.Animated.add(_reactNative.Animated.multiply(distance, spiralRotation.interpolate({
107
+ inputRange: [0, Math.PI * 2],
108
+ outputRange: [Math.sin(angle), Math.sin(angle + Math.PI * 2)]
109
+ })), staggeredProgress.interpolate({
110
+ inputRange: [0, 1],
111
+ outputRange: [0, finalY - radius * Math.sin(angle + Math.PI * 2)]
112
+ }));
114
113
 
115
- // Opacity animation
116
- const itemOpacity = staggeredProgress.interpolate({
117
- inputRange: [0, 0.3, 1],
118
- outputRange: [0, 0.3, 1]
119
- });
120
-
121
- // Scale based on progress
122
- const progressScale = staggeredProgress;
114
+ // Opacity animation
115
+ const itemOpacity = staggeredProgress.interpolate({
116
+ inputRange: [0, 0.3, 1],
117
+ outputRange: [0, 0.3, 1]
118
+ });
119
+ return {
120
+ translateX,
121
+ translateY,
122
+ itemOpacity,
123
+ progressScale: staggeredProgress
124
+ };
125
+ }, [index, totalIcons, iconsProgress, layout]);
123
126
 
124
127
  // Main animated style for position and appearance
125
128
  const animatedStyle = {
126
129
  position: "absolute",
127
- left: CIRCLE_RADIUS - VIEW_SIZE / 2,
130
+ left: layout.circleRadius - VIEW_SIZE / 2,
128
131
  // Center position
129
- top: CIRCLE_RADIUS - VIEW_SIZE / 2,
132
+ top: layout.circleRadius - VIEW_SIZE / 2,
130
133
  // Center position
131
- opacity: itemOpacity,
134
+ opacity: motion.itemOpacity,
132
135
  transform: [{
133
- translateX
136
+ translateX: motion.translateX
134
137
  },
135
138
  // Apply translation from center
136
139
  {
137
- translateY
140
+ translateY: motion.translateY
138
141
  },
139
142
  // Apply translation from center
140
143
  {
141
- scale: _reactNative.Animated.multiply(scale, progressScale)
144
+ scale: _reactNative.Animated.multiply(scale, motion.progressScale)
142
145
  }]
143
146
  };
144
147
 
145
148
  // Check if this is an empty spot (no icon and no iconComponent)
146
149
  const isEmpty = icon.icon === null && !icon.iconComponent;
147
150
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
148
- style: [styles.view, animatedStyle],
151
+ style: [styles.view, animatedStyle, !active && styles.hidden],
149
152
  children: isEmpty ?
150
153
  /*#__PURE__*/
151
154
  // Empty spot - just show a subtle circle
@@ -155,7 +158,7 @@ const DialIcon = ({
155
158
  style: styles.emptyDot
156
159
  })
157
160
  }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
158
- onPress: () => onPress(index),
161
+ onPress: () => onPress(icon),
159
162
  onPressIn: handlePressIn,
160
163
  onPressOut: handlePressOut,
161
164
  style: styles.pressable,
@@ -194,6 +197,9 @@ const styles = _reactNative.StyleSheet.create({
194
197
  justifyContent: "center",
195
198
  alignItems: "center"
196
199
  },
200
+ hidden: {
201
+ display: "none"
202
+ },
197
203
  pressable: {
198
204
  width: "100%",
199
205
  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
+ }