@buoy-gg/core 3.0.1 → 4.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 (51) hide show
  1. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.js +1 -1
  2. package/lib/commonjs/floatingMenu/FloatingDevTools.js +25 -5
  3. package/lib/commonjs/floatingMenu/FloatingMenu.js +3 -7
  4. package/lib/commonjs/floatingMenu/allToolsRegistry.js +193 -0
  5. package/lib/commonjs/floatingMenu/autoExternalSync.js +254 -0
  6. package/lib/commonjs/floatingMenu/defaultConfig.js +1 -1
  7. package/lib/commonjs/floatingMenu/dial/DialDevTools.js +115 -53
  8. package/lib/commonjs/floatingMenu/dial/DialIcon.js +53 -13
  9. package/lib/commonjs/floatingMenu/dial/OnboardingTooltip.js +1 -1
  10. package/lib/module/floatingMenu/DevToolsSettingsModal.js +2 -2
  11. package/lib/module/floatingMenu/FloatingDevTools.js +26 -6
  12. package/lib/module/floatingMenu/FloatingMenu.js +4 -7
  13. package/lib/module/floatingMenu/allToolsRegistry.js +190 -0
  14. package/lib/module/floatingMenu/autoExternalSync.js +248 -0
  15. package/lib/module/floatingMenu/defaultConfig.js +1 -1
  16. package/lib/module/floatingMenu/dial/DialDevTools.js +119 -55
  17. package/lib/module/floatingMenu/dial/DialIcon.js +56 -15
  18. package/lib/module/floatingMenu/dial/OnboardingTooltip.js +2 -1
  19. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
  20. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts +19 -1
  21. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  22. package/lib/typescript/commonjs/floatingMenu/FloatingMenu.d.ts.map +1 -1
  23. package/lib/typescript/commonjs/floatingMenu/allToolsRegistry.d.ts +42 -0
  24. package/lib/typescript/commonjs/floatingMenu/allToolsRegistry.d.ts.map +1 -0
  25. package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts +25 -0
  26. package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts.map +1 -0
  27. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts +1 -1
  28. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts.map +1 -1
  29. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts +7 -0
  30. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/floatingMenu/dial/DialIcon.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/floatingMenu/dial/OnboardingTooltip.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/index.d.ts +1 -0
  34. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  35. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
  36. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts +19 -1
  37. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  38. package/lib/typescript/module/floatingMenu/FloatingMenu.d.ts.map +1 -1
  39. package/lib/typescript/module/floatingMenu/allToolsRegistry.d.ts +42 -0
  40. package/lib/typescript/module/floatingMenu/allToolsRegistry.d.ts.map +1 -0
  41. package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts +25 -0
  42. package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts.map +1 -0
  43. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts +1 -1
  44. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts.map +1 -1
  45. package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts +7 -0
  46. package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  47. package/lib/typescript/module/floatingMenu/dial/DialIcon.d.ts.map +1 -1
  48. package/lib/typescript/module/floatingMenu/dial/OnboardingTooltip.d.ts.map +1 -1
  49. package/lib/typescript/module/index.d.ts +1 -0
  50. package/lib/typescript/module/index.d.ts.map +1 -1
  51. package/package.json +5 -5
@@ -6,10 +6,11 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.DialDevTools = void 0;
7
7
  var _react = require("react");
8
8
  var _reactNative = require("react-native");
9
+ var _sharedUi = require("@buoy-gg/shared-ui");
9
10
  var _DialIcon = require("./DialIcon.js");
11
+ var _allToolsRegistry = require("../allToolsRegistry.js");
10
12
  var _DialPagination = require("./DialPagination.js");
11
13
  var _dialUsageStore = require("./dialUsageStore.js");
12
- var _sharedUi = require("@buoy-gg/shared-ui");
13
14
  var _DevToolsSettingsModal = require("../DevToolsSettingsModal");
14
15
  var _license = require("@buoy-gg/license");
15
16
  var _AppHost = require("../AppHost.js");
@@ -18,17 +19,10 @@ var _floatingToolsCore = require("@buoy-gg/floating-tools-core");
18
19
  var _jsxRuntime = require("react/jsx-runtime");
19
20
  // Icons are provided by installedApps; no direct icon imports here.
20
21
 
21
- const {
22
- width: SCREEN_WIDTH,
23
- height: SCREEN_HEIGHT
24
- } = _reactNative.Dimensions.get("window");
25
-
26
- // Use shared layout calculation from core
27
- const layout = (0, _floatingToolsCore.getDialLayout)({
28
- screenWidth: SCREEN_WIDTH
29
- });
30
- const CIRCLE_SIZE = layout.circleSize;
31
- const BUTTON_SIZE = layout.buttonSize;
22
+ // The circle size depends on the live window width, so it's computed inside
23
+ // the component via useWindowDimensions — a module-scope Dimensions.get
24
+ // snapshot goes stale when the window resizes after load (web/desktop).
25
+ const BUTTON_SIZE = _floatingToolsCore.DIAL_BUTTON_SIZE;
32
26
  const ONBOARDING_STORAGE_KEY = "@react_buoy_settings_tooltip_shown";
33
27
  /** A non-interactive placeholder used to fill out the last dial page. */
34
28
  const createEmptySlot = slotIndex => ({
@@ -58,7 +52,37 @@ const DialDevTools = ({
58
52
  const {
59
53
  open
60
54
  } = (0, _AppHost.useAppHost)();
61
- const isPro = (0, _license.useIsPro)();
55
+ const {
56
+ isPro,
57
+ isWeekendFree
58
+ } = (0, _license.useProAccess)();
59
+
60
+ // Live window size — keeps the dial centered and sized correctly when the
61
+ // window resizes (Electron/web) or the device rotates.
62
+ const {
63
+ width: screenWidth,
64
+ height: screenHeight
65
+ } = (0, _reactNative.useWindowDimensions)();
66
+ const circleSize = (0, _floatingToolsCore.getDialLayout)({
67
+ screenWidth
68
+ }).circleSize;
69
+ const sizeStyles = (0, _react.useMemo)(() => ({
70
+ parent: {
71
+ width: circleSize,
72
+ height: circleSize
73
+ },
74
+ circle: {
75
+ width: circleSize,
76
+ height: circleSize,
77
+ borderRadius: circleSize / 2
78
+ },
79
+ rounded: {
80
+ borderRadius: circleSize / 2
81
+ },
82
+ gridLine: {
83
+ width: circleSize
84
+ }
85
+ }), [circleSize]);
62
86
 
63
87
  // Load persisted settings modal state on mount
64
88
  (0, _react.useEffect)(() => {
@@ -191,6 +215,35 @@ const DialDevTools = ({
191
215
  return map;
192
216
  }, [dialApps, actions, open, onClose]);
193
217
 
218
+ // Tools from the global registry that are NOT installed in this app.
219
+ // These are appended after all available tools so paging "next" eventually
220
+ // reveals them. They are display-only (real icon + real name + "Unavailable").
221
+ const unavailableTools = (0, _react.useMemo)(() => {
222
+ const installedIds = new Set(dialApps.map(a => a.id));
223
+ return _allToolsRegistry.ALL_TOOLS_REGISTRY.filter(t => !installedIds.has(t.id));
224
+ }, [dialApps]);
225
+
226
+ // Build stable IconType entries for each unavailable tool.
227
+ const unavailableIconsById = (0, _react.useMemo)(() => {
228
+ const map = new Map();
229
+ for (const tool of unavailableTools) {
230
+ map.set(tool.id, {
231
+ id: tool.id,
232
+ name: tool.name,
233
+ // Pre-render the icon at the standard dial slot size so it looks correct.
234
+ icon: tool.renderIcon(_floatingToolsCore.DIAL_ICON_SIZE),
235
+ iconComponent: undefined,
236
+ color: tool.color,
237
+ unavailable: true,
238
+ // Tapping an unavailable tool is intentionally a no-op: the dial stays
239
+ // open and no modal launches, signalling the tool is not installed.
240
+ // Future: add a brief Toast with an install link here.
241
+ onPress: () => {}
242
+ });
243
+ }
244
+ return map;
245
+ }, [unavailableTools]);
246
+
194
247
  // Snapshot the usage-ranked order when the dial opens. It stays stable while
195
248
  // open so icons don't jump positions mid-interaction.
196
249
  const [rankedIds, setRankedIds] = (0, _react.useState)(() => (0, _dialUsageStore.getRankedToolIds)(dialApps.map(a => a.id)));
@@ -209,7 +262,9 @@ const DialDevTools = ({
209
262
  cancelled = true;
210
263
  };
211
264
  }, [dialApps]);
212
- const pageCount = Math.max(1, Math.ceil(rankedIds.length / _floatingToolsCore.MAX_DIAL_SLOTS));
265
+
266
+ // Page count spans available tools (usage-ranked) + unavailable tools (appended).
267
+ const pageCount = Math.max(1, Math.ceil((rankedIds.length + unavailableTools.length) / _floatingToolsCore.MAX_DIAL_SLOTS));
213
268
  const [currentPage, setCurrentPage] = (0, _react.useState)(0);
214
269
  const safePage = Math.min(currentPage, pageCount - 1);
215
270
 
@@ -217,13 +272,18 @@ const DialDevTools = ({
217
272
  // snapshotted on open, so each icon's page and slot are fixed for the
218
273
  // session — which lets us mount all icons once and paginate purely by
219
274
  // toggling visibility (no remounts on page change).
275
+ //
276
+ // Order: usage-ranked available tools first, unavailable tools appended at
277
+ // the very end. This guarantees unavailable items always appear on the last
278
+ // pages and never displace available tools.
220
279
  const allDialIcons = (0, _react.useMemo)(() => {
221
- return rankedIds.map(id => iconsById.get(id)).filter(icon => Boolean(icon)).map((icon, i) => ({
280
+ const allIds = [...rankedIds, ...unavailableTools.map(t => t.id)];
281
+ return allIds.map(id => iconsById.get(id) ?? unavailableIconsById.get(id)).filter(icon => Boolean(icon)).map((icon, i) => ({
222
282
  icon,
223
283
  page: Math.floor(i / _floatingToolsCore.MAX_DIAL_SLOTS),
224
284
  slot: i % _floatingToolsCore.MAX_DIAL_SLOTS
225
285
  }));
226
- }, [rankedIds, iconsById]);
286
+ }, [rankedIds, iconsById, unavailableTools, unavailableIconsById]);
227
287
 
228
288
  // Empty slot indices for the current page (only the last page can be
229
289
  // partial). These are cheap placeholder dots.
@@ -425,6 +485,10 @@ const DialDevTools = ({
425
485
  });
426
486
  };
427
487
  const handleIconPress = icon => {
488
+ // Unavailable tools are display-only. Return early so the dial stays open
489
+ // and no modal or animation is triggered. The dimmed appearance already
490
+ // communicates that the tool is not installed.
491
+ if (icon.unavailable) return;
428
492
  setSelectedIcon(1);
429
493
  const interaction = _floatingToolsCore.dialAnimationConfig?.interaction ?? {
430
494
  iconSelect: {
@@ -485,13 +549,13 @@ const DialDevTools = ({
485
549
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
486
550
  style: [styles.backdrop, backdropAnimatedStyle],
487
551
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
488
- style: _reactNative.StyleSheet.absoluteFillObject,
552
+ style: _sharedUi.absoluteFill,
489
553
  onPress: handleClose
490
554
  })
491
555
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Animated.View, {
492
- style: [styles.parent, {
556
+ style: [styles.parent, sizeStyles.parent, {
493
557
  position: "absolute",
494
- left: (SCREEN_WIDTH - CIRCLE_SIZE) / 2,
558
+ left: (screenWidth - circleSize) / 2,
495
559
  bottom: 80,
496
560
  transform: [{
497
561
  scale: dialScale
@@ -503,21 +567,21 @@ const DialDevTools = ({
503
567
  }]
504
568
  }],
505
569
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Animated.View, {
506
- style: styles.circle,
570
+ style: [styles.circle, sizeStyles.circle],
507
571
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
508
- style: styles.gradientBackground,
572
+ style: [styles.gradientBackground, sizeStyles.rounded],
509
573
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
510
- style: styles.gradientLayer1
574
+ style: [styles.gradientLayer1, sizeStyles.rounded]
511
575
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
512
- style: styles.gradientLayer2
576
+ style: [styles.gradientLayer2, sizeStyles.rounded]
513
577
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
514
- style: styles.gradientLayer3
578
+ style: [styles.gradientLayer3, sizeStyles.rounded]
515
579
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
516
580
  style: styles.gridPattern,
517
581
  children: Array.from({
518
582
  length: 6
519
583
  }).map((_, i) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
520
- style: [styles.gridLine, {
584
+ style: [styles.gridLine, sizeStyles.gridLine, {
521
585
  transform: [{
522
586
  rotate: `${i * 60}deg`
523
587
  }]
@@ -592,8 +656,8 @@ const DialDevTools = ({
592
656
  style: styles.centerText,
593
657
  children: "BUOY"
594
658
  }), isPro && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
595
- style: styles.proText,
596
- children: "PRO"
659
+ style: [styles.proText, isWeekendFree && styles.weekendText],
660
+ children: isWeekendFree ? "WEEKEND" : "PRO"
597
661
  })]
598
662
  })
599
663
  })
@@ -608,11 +672,11 @@ const DialDevTools = ({
608
672
  onNext: () => handlePageChange(safePage + 1),
609
673
  animatedStyle: {
610
674
  position: "absolute",
611
- left: (SCREEN_WIDTH - CIRCLE_SIZE) / 2,
612
- // Circle's bottom edge sits at bottom: 80 -> SCREEN_HEIGHT - 80
675
+ left: (screenWidth - circleSize) / 2,
676
+ // Circle's bottom edge sits at bottom: 80 -> screenHeight - 80
613
677
  // from the top. Place the pager 16px below that edge.
614
- top: SCREEN_HEIGHT - 80 + 16,
615
- width: CIRCLE_SIZE,
678
+ top: screenHeight - 80 + 16,
679
+ width: circleSize,
616
680
  opacity: dialScale,
617
681
  transform: [{
618
682
  scale: dialScale
@@ -634,23 +698,20 @@ const DialDevTools = ({
634
698
  exports.DialDevTools = DialDevTools;
635
699
  const styles = _reactNative.StyleSheet.create({
636
700
  container: {
637
- ..._reactNative.StyleSheet.absoluteFillObject,
701
+ ..._sharedUi.absoluteFill,
638
702
  zIndex: 9999
639
703
  },
640
704
  backdrop: {
641
- ..._reactNative.StyleSheet.absoluteFillObject,
705
+ ..._sharedUi.absoluteFill,
642
706
  backgroundColor: _floatingToolsCore.dialColors.dialBackdrop
643
707
  },
708
+ // width/height/borderRadius for the circle pieces come from sizeStyles —
709
+ // they track the live window width.
644
710
  parent: {
645
- width: CIRCLE_SIZE,
646
- height: CIRCLE_SIZE,
647
711
  alignItems: "center",
648
712
  justifyContent: "center"
649
713
  },
650
714
  circle: {
651
- width: CIRCLE_SIZE,
652
- height: CIRCLE_SIZE,
653
- borderRadius: CIRCLE_SIZE / 2,
654
715
  position: "absolute",
655
716
  backgroundColor: "transparent",
656
717
  borderWidth: 1,
@@ -667,41 +728,36 @@ const styles = _reactNative.StyleSheet.create({
667
728
  gradientBackground: {
668
729
  width: "100%",
669
730
  height: "100%",
670
- borderRadius: CIRCLE_SIZE / 2,
671
731
  position: "relative",
672
732
  backgroundColor: _floatingToolsCore.dialColors.dialBackground,
673
733
  overflow: "hidden"
674
734
  },
675
735
  gradientLayer1: {
676
- ..._reactNative.StyleSheet.absoluteFillObject,
736
+ ..._sharedUi.absoluteFill,
677
737
  backgroundColor: _floatingToolsCore.dialColors.dialGradient1,
678
- opacity: 0.6,
679
- borderRadius: CIRCLE_SIZE / 2
738
+ opacity: 0.6
680
739
  },
681
740
  gradientLayer2: {
682
- ..._reactNative.StyleSheet.absoluteFillObject,
741
+ ..._sharedUi.absoluteFill,
683
742
  backgroundColor: _floatingToolsCore.dialColors.dialGradient2,
684
743
  opacity: 0.4,
685
744
  top: "30%",
686
- left: "30%",
687
- borderRadius: CIRCLE_SIZE / 2
745
+ left: "30%"
688
746
  },
689
747
  gradientLayer3: {
690
- ..._reactNative.StyleSheet.absoluteFillObject,
748
+ ..._sharedUi.absoluteFill,
691
749
  backgroundColor: _floatingToolsCore.dialColors.dialGradient3,
692
750
  opacity: 0.3,
693
751
  top: "50%",
694
- left: "50%",
695
- borderRadius: CIRCLE_SIZE / 2
752
+ left: "50%"
696
753
  },
697
754
  gridPattern: {
698
- ..._reactNative.StyleSheet.absoluteFillObject,
755
+ ..._sharedUi.absoluteFill,
699
756
  alignItems: "center",
700
757
  justifyContent: "center"
701
758
  },
702
759
  gridLine: {
703
760
  position: "absolute",
704
- width: CIRCLE_SIZE,
705
761
  height: 1,
706
762
  backgroundColor: _floatingToolsCore.dialColors.dialGridLine
707
763
  },
@@ -727,13 +783,13 @@ const styles = _reactNative.StyleSheet.create({
727
783
  overflow: "hidden"
728
784
  },
729
785
  buttonGradientLayer1: {
730
- ..._reactNative.StyleSheet.absoluteFillObject,
786
+ ..._sharedUi.absoluteFill,
731
787
  backgroundColor: _floatingToolsCore.dialColors.dialGradient1,
732
788
  opacity: 0.5,
733
789
  borderRadius: BUTTON_SIZE
734
790
  },
735
791
  buttonGradientLayer2: {
736
- ..._reactNative.StyleSheet.absoluteFillObject,
792
+ ..._sharedUi.absoluteFill,
737
793
  backgroundColor: _floatingToolsCore.dialColors.dialGradient2,
738
794
  opacity: 0.3,
739
795
  top: "20%",
@@ -741,7 +797,7 @@ const styles = _reactNative.StyleSheet.create({
741
797
  borderRadius: BUTTON_SIZE
742
798
  },
743
799
  buttonGradientLayer3: {
744
- ..._reactNative.StyleSheet.absoluteFillObject,
800
+ ..._sharedUi.absoluteFill,
745
801
  backgroundColor: _floatingToolsCore.dialColors.dialGradient3,
746
802
  opacity: 0.2,
747
803
  top: "40%",
@@ -807,5 +863,11 @@ const styles = _reactNative.StyleSheet.create({
807
863
  height: 0
808
864
  },
809
865
  textShadowRadius: 4
866
+ },
867
+ // Weekend Pass variant — violet, tighter spacing so "WEEKEND" fits the dial.
868
+ weekendText: {
869
+ color: "#BF5AF2",
870
+ letterSpacing: 1,
871
+ textShadowColor: "#BF5AF2"
810
872
  }
811
873
  });
@@ -8,16 +8,9 @@ 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_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;
21
14
  const DialIcon = ({
22
15
  index,
23
16
  icon,
@@ -28,6 +21,12 @@ const DialIcon = ({
28
21
  }) => {
29
22
  // Animation values - using interpolation for better performance
30
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]);
31
30
 
32
31
  // Hover animation on press in/out - using shared config
33
32
  // Fallback values in case dialAnimationConfig hasn't loaded yet
@@ -123,14 +122,14 @@ const DialIcon = ({
123
122
  itemOpacity,
124
123
  progressScale: staggeredProgress
125
124
  };
126
- }, [index, totalIcons, iconsProgress]);
125
+ }, [index, totalIcons, iconsProgress, layout]);
127
126
 
128
127
  // Main animated style for position and appearance
129
128
  const animatedStyle = {
130
129
  position: "absolute",
131
- left: CIRCLE_RADIUS - VIEW_SIZE / 2,
130
+ left: layout.circleRadius - VIEW_SIZE / 2,
132
131
  // Center position
133
- top: CIRCLE_RADIUS - VIEW_SIZE / 2,
132
+ top: layout.circleRadius - VIEW_SIZE / 2,
134
133
  // Center position
135
134
  opacity: motion.itemOpacity,
136
135
  transform: [{
@@ -158,6 +157,32 @@ const DialIcon = ({
158
157
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
159
158
  style: styles.emptyDot
160
159
  })
160
+ }) : icon.unavailable ?
161
+ /*#__PURE__*/
162
+ // ── Unavailable tool ──────────────────────────────────────────────────
163
+ // Renders the real icon + real name at NORMAL styling (visually identical
164
+ // to an installed tool). The ONLY signal that it's unavailable is the small
165
+ // muted "Unavailable" caption under the name. It's non-interactive: no
166
+ // Pressable, and the dial stays open when tapped because
167
+ // DialDevTools.handleIconPress returns early for unavailable icons.
168
+ (0, _jsxRuntime.jsxs)(_reactNative.View, {
169
+ style: styles.pressable,
170
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
171
+ style: styles.iconWrapper,
172
+ children: icon.icon
173
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
174
+ style: styles.label,
175
+ numberOfLines: 1,
176
+ adjustsFontSizeToFit: true,
177
+ minimumFontScale: 0.7,
178
+ children: icon.name.toUpperCase()
179
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
180
+ style: styles.unavailableCaption,
181
+ numberOfLines: 1,
182
+ adjustsFontSizeToFit: true,
183
+ minimumFontScale: 0.6,
184
+ children: "Unavailable"
185
+ })]
161
186
  }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
162
187
  onPress: () => onPress(icon),
163
188
  onPressIn: handlePressIn,
@@ -249,5 +274,20 @@ const styles = _reactNative.StyleSheet.create({
249
274
  backgroundColor: _floatingToolsCore.dialColors.emptyDotBackground,
250
275
  borderWidth: _floatingToolsCore.dialStyles.emptySlot.borderWidth,
251
276
  borderColor: _floatingToolsCore.dialColors.emptyDotBorder
277
+ },
278
+ // ── Unavailable tool styles ────────────────────────────────────────────────
279
+ /**
280
+ * Small muted caption shown under the (normal-styled) tool name — the only
281
+ * signal that the tool is not installed. Kept compact (smaller than the label)
282
+ * so the extra line doesn't disturb the arc layout. Uses the shared muted
283
+ * theme token rather than a hardcoded color.
284
+ */
285
+ unavailableCaption: {
286
+ fontSize: _floatingToolsCore.dialStyles.icon.label.fontSize - 1,
287
+ fontWeight: _floatingToolsCore.dialStyles.icon.label.fontWeight,
288
+ letterSpacing: _floatingToolsCore.dialStyles.icon.label.letterSpacing,
289
+ fontFamily: _floatingToolsCore.dialStyles.icon.label.fontFamily,
290
+ color: _floatingToolsCore.floatingToolsColors.muted,
291
+ textTransform: "uppercase"
252
292
  }
253
293
  });
@@ -133,7 +133,7 @@ const OnboardingTooltip = ({
133
133
  exports.OnboardingTooltip = OnboardingTooltip;
134
134
  const styles = _reactNative.StyleSheet.create({
135
135
  container: {
136
- ..._reactNative.StyleSheet.absoluteFillObject,
136
+ ..._sharedUi.absoluteFill,
137
137
  alignItems: "center",
138
138
  justifyContent: "center",
139
139
  pointerEvents: "box-none"
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  import { useState, useEffect, useCallback, useMemo } from "react";
4
- import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Dimensions } from "react-native";
4
+ import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Dimensions, Platform } from "react-native";
5
5
  import { settingsBus } from "./settingsBus.js";
6
6
  import { SentryBugIcon, WifiCircuitIcon, ImageOverlayIcon, RenderCountIcon, Info, Layers, ChevronRightIcon, ChevronDown, getStorageBackendType, persistentStorage, Database, Trash2, CheckCircle2, AlertTriangle, Zap, FileText, HardDrive, Copy, FileCode, copyToClipboard, LicenseEntryModal, SectionHeader } from "@buoy-gg/shared-ui";
7
7
  import { EnvIcon, StorageIcon, RoutesIcon, NetworkIcon, QueryIcon, HighlighterIcon, ReduxIcon, EventsIcon } from "@buoy-gg/floating-tools-core";
@@ -809,7 +809,7 @@ export const DevToolsSettingsModal = ({
809
809
  children: clearSuccess ? "CLEARED" : isClearing ? "CLEARING..." : "CLEAR ALL SETTINGS"
810
810
  })]
811
811
  })]
812
- }), renderGlobalSettingCard("enableSharedModalDimensions", "SHARED MODAL SIZE", "MODAL", "Sync dimensions across all tools", "When enabled, all tool modals will share the same size and position. Resizing one modal will affect all others. When disabled, each tool remembers its own size and position independently.", "Keep OFF for the best experience. This allows you to customize each tool's modal size separately. Enable only if you prefer uniform modal sizes across all dev tools."), renderGlobalSettingCard("expandableWindowControls", "EXPAND CONTROLS", "MODAL", "iPad-style expandable window buttons", "When enabled, the window control buttons (minimize, toggle mode, close) start as small dots and expand into larger, easy-to-tap buttons when pressed — similar to iPad window controls. When disabled, buttons are directly tappable at their small size.", "Keep ON for touch devices where the small buttons are hard to press. Turn OFF if you prefer direct single-tap access (e.g. when using a mouse or simulator)."), isDevelopmentMode && /*#__PURE__*/_jsxs(View, {
812
+ }), renderGlobalSettingCard("enableSharedModalDimensions", "SHARED MODAL SIZE", "MODAL", "Sync dimensions across all tools", "When enabled, all tool modals will share the same size and position. Resizing one modal will affect all others. When disabled, each tool remembers its own size and position independently.", "Keep OFF for the best experience. This allows you to customize each tool's modal size separately. Enable only if you prefer uniform modal sizes across all dev tools."), Platform.OS !== "web" && renderGlobalSettingCard("expandableWindowControls", "EXPAND CONTROLS", "MODAL", "iPad-style expandable window buttons", "When enabled, the window control buttons (minimize, toggle mode, close) start as small dots and expand into larger, easy-to-tap buttons when pressed — similar to iPad window controls. When disabled, buttons are directly tappable at their small size.", "Keep ON for touch devices where the small buttons are hard to press. Turn OFF if you prefer direct single-tap access (e.g. when using a mouse or simulator)."), isDevelopmentMode && /*#__PURE__*/_jsxs(View, {
813
813
  style: styles.exportConfigCard,
814
814
  children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
815
815
  activeOpacity: 0.85,
@@ -11,7 +11,8 @@ import { HintsProvider, LicenseEntryModal, normalizeEnvironment } from "@buoy-gg
11
11
  import { MinimizedToolsProvider } from "./MinimizedToolsContext.js";
12
12
  import { validateDialConfig } from "./defaultConfig.js";
13
13
  import { DefaultConfigProvider } from "./DefaultConfigContext.js";
14
- import { LicenseManager, useIsPro, useLicense } from "@buoy-gg/license";
14
+ import { LicenseManager, useProAccess, useLicense } from "@buoy-gg/license";
15
+ import { AutoExternalSync, isExternalSyncAvailable } from "./autoExternalSync.js";
15
16
 
16
17
  /**
17
18
  * Environment variable configuration
@@ -109,11 +110,18 @@ export const FloatingDevTools = ({
109
110
  licenseKey: licenseKeyProp,
110
111
  zustandStores,
111
112
  environment,
113
+ externalSync = true,
112
114
  ...props
113
115
  }) => {
114
116
  const resolvedEnvironment = environment ?? normalizeEnvironment(process.env.NODE_ENV ?? "dev");
115
- // Check Pro status for production gating
116
- const isPro = useIsPro();
117
+ // Production gating + the device→dashboard license handshake must use a REAL
118
+ // license only — NOT the free Weekend Pass. Otherwise shipping Buoy would
119
+ // expose the devtools to end users every weekend, and the dashboard would try
120
+ // to adopt a non-existent key. (Feature unlocks inside the tools still honor
121
+ // the Weekend Pass via their own useIsPro().)
122
+ const {
123
+ isLicensed
124
+ } = useProAccess();
117
125
 
118
126
  // Get full license state for requireLicense gating
119
127
  const {
@@ -122,10 +130,17 @@ export const FloatingDevTools = ({
122
130
  } = useLicense();
123
131
  const [showLicenseModal, setShowLicenseModal] = useState(false);
124
132
 
125
- // Initialize license manager on mount, and set/clear license key when prop changes
133
+ // Initialize license manager on mount, and set/clear license key when prop changes.
134
+ // The `licenseKey` prop is the source of truth WHEN PROVIDED:
135
+ // - non-empty → validate + activate that key
136
+ // - "" (blank) → EXPLICITLY clear the cached license (key was removed)
137
+ // - undefined → don't touch it (the app may set a key via Buoy.init())
126
138
  useEffect(() => {
127
139
  if (licenseKeyProp && licenseKeyProp.trim() !== "") {
128
140
  LicenseManager.initialize().then(() => LicenseManager.setLicenseKey(licenseKeyProp)).catch(() => {});
141
+ } else if (licenseKeyProp === "") {
142
+ // Explicit removal: wipe the 30-day cache so Pro actually turns off.
143
+ LicenseManager.initialize().then(() => LicenseManager.clearLicense()).catch(() => {});
129
144
  } else {
130
145
  LicenseManager.initialize();
131
146
  }
@@ -302,7 +317,7 @@ export const FloatingDevTools = ({
302
317
  // Gate: Free tier only works in development mode
303
318
  // Pro users can use DevTools everywhere (dev + production)
304
319
  const isDevelopment = typeof __DEV__ !== "undefined" && __DEV__;
305
- if (!isDevelopment && !isPro) {
320
+ if (!isDevelopment && !isLicensed) {
306
321
  // Free user in production - don't render DevTools
307
322
  return null;
308
323
  }
@@ -338,7 +353,12 @@ export const FloatingDevTools = ({
338
353
  environment: resolvedEnvironment
339
354
  }), /*#__PURE__*/_jsx(AppOverlay, {})]
340
355
  })
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, {})]
356
+ }), 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, {}), isDevelopment && externalSync !== false && isExternalSyncAvailable() && /*#__PURE__*/_jsx(AutoExternalSync, {
357
+ options: typeof externalSync === "object" ? externalSync : undefined,
358
+ requiredEnvVars: requiredEnvVars,
359
+ isPro: isLicensed,
360
+ licenseKey: licenseKey
361
+ })]
342
362
  })
343
363
  })
344
364
  })
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  import React, { useEffect, useMemo, useRef, useState } from "react";
4
- import { TouchableOpacity, StyleSheet, View, Dimensions } from "react-native";
4
+ import { TouchableOpacity, StyleSheet, View } from "react-native";
5
+ import { absoluteFill } from "@buoy-gg/shared-ui";
5
6
  import { FloatingTools, UserStatus } from "./floatingTools";
6
7
  import { DialDevTools } from "./dial/DialDevTools";
7
8
  import { EnvironmentIndicator, persistentStorage, useHintsDisabled, devToolsStorageKeys, buoyColors } from "@buoy-gg/shared-ui";
@@ -22,10 +23,6 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
22
23
  */
23
24
  const FLOATING_MENU_ONBOARDING_KEY = "@react_buoy_floating_menu_tooltip_shown";
24
25
  const ONBOARDING_STEP_KEY = "@react_buoy_onboarding_step";
25
- const {
26
- width: SCREEN_WIDTH,
27
- height: SCREEN_HEIGHT
28
- } = Dimensions.get("window");
29
26
  export const FloatingMenu = ({
30
27
  apps,
31
28
  state,
@@ -322,11 +319,11 @@ const styles = StyleSheet.create({
322
319
  fontWeight: "900"
323
320
  },
324
321
  onboardingContainer: {
325
- ...StyleSheet.absoluteFillObject,
322
+ ...absoluteFill,
326
323
  zIndex: 10000
327
324
  },
328
325
  onboardingBackdrop: {
329
- ...StyleSheet.absoluteFillObject,
326
+ ...absoluteFill,
330
327
  backgroundColor: "rgba(0, 0, 0, 0.85)"
331
328
  }
332
329
  });