@buoy-gg/core 7.0.35 → 7.0.38

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 (111) hide show
  1. package/README.md +57 -66
  2. package/bin/buoy.js +71 -20
  3. package/lib/commonjs/Buoy.js +3 -16
  4. package/lib/commonjs/floatingMenu/AppHost.js +1 -1
  5. package/lib/commonjs/floatingMenu/AppHost.web.js +1 -1
  6. package/lib/commonjs/floatingMenu/AppIcon.web.js +53 -0
  7. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.js +12 -5
  8. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.web.js +14 -34
  9. package/lib/commonjs/floatingMenu/FloatingDevTools.js +32 -48
  10. package/lib/commonjs/floatingMenu/FloatingDevTools.web.js +24 -4
  11. package/lib/commonjs/floatingMenu/FloatingMenu.js +1 -1
  12. package/lib/commonjs/floatingMenu/MinimizedToolsStack.js +83 -4
  13. package/lib/commonjs/floatingMenu/MinimizedToolsStack.web.js +90 -6
  14. package/lib/commonjs/floatingMenu/ToolHost.js +1 -0
  15. package/lib/commonjs/floatingMenu/autoExternalSync.js +3 -2
  16. package/lib/commonjs/floatingMenu/dial/DialDevTools.js +43 -10
  17. package/lib/commonjs/floatingMenu/dial/DialDevTools.web.js +18 -5
  18. package/lib/commonjs/floatingMenu/dial/dialUsageStore.js +13 -17
  19. package/lib/commonjs/floatingMenu/dial/onboardingConstants.js +37 -65
  20. package/lib/commonjs/floatingMenu/dial/onboardingConstants.web.js +37 -29
  21. package/lib/commonjs/floatingMenu/floatingTools.js +23 -889
  22. package/lib/commonjs/floatingMenu/floatingTools.web.js +24 -514
  23. package/lib/commonjs/floatingMenu/optionalRequire.js +4 -0
  24. package/lib/commonjs/telemetry/installPing.js +7 -0
  25. package/lib/commonjs/version.js +1 -1
  26. package/lib/module/Buoy.js +3 -16
  27. package/lib/module/floatingMenu/AppHost.js +1 -1
  28. package/lib/module/floatingMenu/AppHost.web.js +1 -1
  29. package/lib/module/floatingMenu/AppIcon.web.js +47 -0
  30. package/lib/module/floatingMenu/DevToolsSettingsModal.js +13 -6
  31. package/lib/module/floatingMenu/DevToolsSettingsModal.web.js +15 -35
  32. package/lib/module/floatingMenu/FloatingDevTools.js +35 -49
  33. package/lib/module/floatingMenu/FloatingDevTools.web.js +24 -3
  34. package/lib/module/floatingMenu/FloatingMenu.js +1 -1
  35. package/lib/module/floatingMenu/MinimizedToolsStack.js +88 -8
  36. package/lib/module/floatingMenu/MinimizedToolsStack.web.js +92 -8
  37. package/lib/module/floatingMenu/ToolHost.js +1 -0
  38. package/lib/module/floatingMenu/autoExternalSync.js +3 -2
  39. package/lib/module/floatingMenu/dial/DialDevTools.js +43 -10
  40. package/lib/module/floatingMenu/dial/DialDevTools.web.js +18 -5
  41. package/lib/module/floatingMenu/dial/dialUsageStore.js +14 -18
  42. package/lib/module/floatingMenu/dial/onboardingConstants.js +1 -62
  43. package/lib/module/floatingMenu/dial/onboardingConstants.web.js +1 -25
  44. package/lib/module/floatingMenu/floatingTools.js +15 -890
  45. package/lib/module/floatingMenu/floatingTools.web.js +1 -517
  46. package/lib/module/floatingMenu/optionalRequire.js +4 -0
  47. package/lib/module/telemetry/installPing.js +7 -0
  48. package/lib/module/version.js +1 -1
  49. package/lib/typescript/commonjs/Buoy.d.ts +14 -3
  50. package/lib/typescript/commonjs/Buoy.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/floatingMenu/AppIcon.web.d.ts +14 -0
  52. package/lib/typescript/commonjs/floatingMenu/AppIcon.web.d.ts.map +1 -0
  53. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
  54. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts +4 -14
  56. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.web.d.ts +4 -19
  58. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsStack.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsStack.web.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/floatingMenu/ToolHost.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts +3 -4
  63. package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/floatingMenu/dial/dialUsageStore.d.ts +3 -15
  67. package/lib/typescript/commonjs/floatingMenu/dial/dialUsageStore.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/floatingMenu/dial/onboardingConstants.d.ts +1 -29
  69. package/lib/typescript/commonjs/floatingMenu/dial/onboardingConstants.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/floatingMenu/dial/onboardingConstants.web.d.ts +1 -12
  71. package/lib/typescript/commonjs/floatingMenu/dial/onboardingConstants.web.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/floatingMenu/floatingTools.d.ts +6 -55
  73. package/lib/typescript/commonjs/floatingMenu/floatingTools.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/floatingMenu/floatingTools.web.d.ts +3 -53
  75. package/lib/typescript/commonjs/floatingMenu/floatingTools.web.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/floatingMenu/optionalRequire.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/telemetry/installPing.d.ts +8 -1
  78. package/lib/typescript/commonjs/telemetry/installPing.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/version.d.ts +1 -1
  80. package/lib/typescript/module/Buoy.d.ts +14 -3
  81. package/lib/typescript/module/Buoy.d.ts.map +1 -1
  82. package/lib/typescript/module/floatingMenu/AppIcon.web.d.ts +14 -0
  83. package/lib/typescript/module/floatingMenu/AppIcon.web.d.ts.map +1 -0
  84. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
  85. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -1
  86. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts +4 -14
  87. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  88. package/lib/typescript/module/floatingMenu/FloatingDevTools.web.d.ts +4 -19
  89. package/lib/typescript/module/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
  90. package/lib/typescript/module/floatingMenu/MinimizedToolsStack.d.ts.map +1 -1
  91. package/lib/typescript/module/floatingMenu/MinimizedToolsStack.web.d.ts.map +1 -1
  92. package/lib/typescript/module/floatingMenu/ToolHost.d.ts.map +1 -1
  93. package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts +3 -4
  94. package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts.map +1 -1
  95. package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  96. package/lib/typescript/module/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -1
  97. package/lib/typescript/module/floatingMenu/dial/dialUsageStore.d.ts +3 -15
  98. package/lib/typescript/module/floatingMenu/dial/dialUsageStore.d.ts.map +1 -1
  99. package/lib/typescript/module/floatingMenu/dial/onboardingConstants.d.ts +1 -29
  100. package/lib/typescript/module/floatingMenu/dial/onboardingConstants.d.ts.map +1 -1
  101. package/lib/typescript/module/floatingMenu/dial/onboardingConstants.web.d.ts +1 -12
  102. package/lib/typescript/module/floatingMenu/dial/onboardingConstants.web.d.ts.map +1 -1
  103. package/lib/typescript/module/floatingMenu/floatingTools.d.ts +6 -55
  104. package/lib/typescript/module/floatingMenu/floatingTools.d.ts.map +1 -1
  105. package/lib/typescript/module/floatingMenu/floatingTools.web.d.ts +3 -53
  106. package/lib/typescript/module/floatingMenu/floatingTools.web.d.ts.map +1 -1
  107. package/lib/typescript/module/floatingMenu/optionalRequire.d.ts.map +1 -1
  108. package/lib/typescript/module/telemetry/installPing.d.ts +8 -1
  109. package/lib/typescript/module/telemetry/installPing.d.ts.map +1 -1
  110. package/lib/typescript/module/version.d.ts +1 -1
  111. package/package.json +6 -6
@@ -16,8 +16,41 @@
16
16
  * used before this replaced it.
17
17
  */
18
18
 
19
- import { useCallback, useEffect, useRef, useState } from 'react';
20
- import { persistentStorage } from '@buoy-gg/shared-ui';
19
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
20
+ import { persistentStorage, getToolAttention, subscribeToolAttention } from '@buoy-gg/shared-ui';
21
+
22
+ /** Twin of the native hook — see MinimizedToolsStack.tsx. */
23
+ function useToolAttention(toolId) {
24
+ return useSyncExternalStore(subscribeToolAttention, () => getToolAttention(toolId), () => getToolAttention(toolId));
25
+ }
26
+ const ATTENTION_COLORS = {
27
+ needsInput: '#FF8329',
28
+ done: '#20C997',
29
+ failed: '#FF3B30'
30
+ };
31
+ const ATTENTION_RANK = {
32
+ needsInput: 3,
33
+ failed: 2,
34
+ done: 1
35
+ };
36
+ const strongestCache = new Map();
37
+ function strongest(key) {
38
+ let best = null;
39
+ for (const id of key ? key.split('|') : []) {
40
+ const a = getToolAttention(id);
41
+ if (a && (!best || ATTENTION_RANK[a.kind] > ATTENTION_RANK[best.kind])) best = a;
42
+ }
43
+ const prev = strongestCache.get(key);
44
+ if (prev && best && prev.kind === best.kind && prev.label === best.label) return prev;
45
+ if (!prev && !best) return null;
46
+ strongestCache.set(key, best);
47
+ return best;
48
+ }
49
+ /** Twin of the native hook: the strongest attention across the folded dock. */
50
+ function useStrongestAttention(toolIds) {
51
+ const key = toolIds.join('|');
52
+ return useSyncExternalStore(subscribeToolAttention, () => strongest(key), () => strongest(key));
53
+ }
21
54
  import { useAppHost } from "./AppHost.web.js";
22
55
 
23
56
  // native MinimizedToolsStack.tsx constants
@@ -58,11 +91,13 @@ function ChevronGlyph({
58
91
  });
59
92
  }
60
93
  function GlitchToolButton({
94
+ id,
61
95
  title,
62
96
  icon,
63
97
  index,
64
98
  onPress
65
99
  }) {
100
+ const attention = useToolAttention(id);
66
101
  const innerRef = useRef(null);
67
102
  useEffect(() => {
68
103
  let mounted = true;
@@ -113,9 +148,9 @@ function GlitchToolButton({
113
148
  clearTimeout(timer);
114
149
  };
115
150
  }, [index]);
116
- return /*#__PURE__*/_jsx("button", {
151
+ return /*#__PURE__*/_jsxs("button", {
117
152
  type: "button",
118
- "aria-label": `Restore ${title}`,
153
+ "aria-label": attention ? `${title} — ${attention.label}. Restore` : `Restore ${title}`,
119
154
  onClick: onPress,
120
155
  style: {
121
156
  width: TOOL_ITEM_SIZE,
@@ -126,9 +161,10 @@ function GlitchToolButton({
126
161
  background: 'none',
127
162
  border: 'none',
128
163
  padding: 0,
129
- cursor: 'pointer'
164
+ cursor: 'pointer',
165
+ position: 'relative'
130
166
  },
131
- children: /*#__PURE__*/_jsx("div", {
167
+ children: [/*#__PURE__*/_jsx("div", {
132
168
  ref: innerRef,
133
169
  style: {
134
170
  display: 'flex',
@@ -136,7 +172,30 @@ function GlitchToolButton({
136
172
  justifyContent: 'center'
137
173
  },
138
174
  children: icon
139
- })
175
+ }), attention ? /*#__PURE__*/_jsx("span", {
176
+ "aria-hidden": true,
177
+ "data-testid": `buoy-dock-badge-${id}-${attention.kind}`,
178
+ style: {
179
+ position: 'absolute',
180
+ top: 0,
181
+ right: 0,
182
+ minWidth: 12,
183
+ height: 12,
184
+ borderRadius: 6,
185
+ padding: '0 2px',
186
+ boxSizing: 'border-box',
187
+ background: ATTENTION_COLORS[attention.kind],
188
+ border: '1.5px solid #0C0C0C',
189
+ color: '#000',
190
+ fontSize: 8,
191
+ fontWeight: 800,
192
+ lineHeight: '9px',
193
+ display: 'flex',
194
+ alignItems: 'center',
195
+ justifyContent: 'center'
196
+ },
197
+ children: attention.kind === 'needsInput' ? '!' : ''
198
+ }) : null]
140
199
  });
141
200
  }
142
201
  export function MinimizedToolsStack({
@@ -149,6 +208,7 @@ export function MinimizedToolsStack({
149
208
  const minimized = openApps.filter(app => app.minimized && app.launchMode !== 'toggle-only');
150
209
  const [isExpanded, setIsExpanded] = useState(false);
151
210
  const [isStateRestored, setIsStateRestored] = useState(false);
211
+ const peekAttention = useStrongestAttention(minimized.map(app => app.id));
152
212
 
153
213
  // Restore + persist the expanded flag (native ExpandablePopover persistState).
154
214
  useEffect(() => {
@@ -216,6 +276,7 @@ export function MinimizedToolsStack({
216
276
  boxSizing: 'border-box'
217
277
  },
218
278
  children: [[...minimized].reverse().map((app, index) => /*#__PURE__*/_jsx(GlitchToolButton, {
279
+ id: app.id,
219
280
  title: app.title || app.id,
220
281
  icon: app.icon ?? /*#__PURE__*/_jsx("span", {
221
282
  style: {
@@ -270,7 +331,30 @@ export function MinimizedToolsStack({
270
331
  pointerEvents: isExpanded ? 'none' : 'auto',
271
332
  ...frame
272
333
  },
273
- children: [/*#__PURE__*/_jsx(ChevronGlyph, {}), minimized.length > 1 && /*#__PURE__*/_jsx("span", {
334
+ children: [/*#__PURE__*/_jsx(ChevronGlyph, {}), peekAttention ? /*#__PURE__*/_jsx("span", {
335
+ "aria-hidden": true,
336
+ "data-testid": `buoy-dock-peek-attention`,
337
+ style: {
338
+ position: 'absolute',
339
+ top: 2,
340
+ right: 2,
341
+ minWidth: 11,
342
+ height: 11,
343
+ borderRadius: 6,
344
+ padding: '0 2px',
345
+ boxSizing: 'border-box',
346
+ background: ATTENTION_COLORS[peekAttention.kind],
347
+ border: '1.5px solid #0C0C0C',
348
+ color: '#000',
349
+ fontSize: 8,
350
+ fontWeight: 800,
351
+ lineHeight: '9px',
352
+ display: 'flex',
353
+ alignItems: 'center',
354
+ justifyContent: 'center'
355
+ },
356
+ children: peekAttention.kind === 'needsInput' ? '!' : ''
357
+ }) : null, minimized.length > 1 && /*#__PURE__*/_jsx("span", {
274
358
  style: {
275
359
  fontSize: 9,
276
360
  fontWeight: 600,
@@ -189,6 +189,7 @@ export function ToolModalShell({
189
189
  subtitle: subtitle
190
190
  }), /*#__PURE__*/_jsxs(ModalHeader.Actions, {
191
191
  children: [!isSearchActive && actions, searchable && !isSearchActive && /*#__PURE__*/_jsx(HeaderSearchButton, {
192
+ variant: "chip",
192
193
  testID: `tool-search-${app.id}`,
193
194
  onPress: () => setIsSearchActive(true)
194
195
  })]
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { useEffect, useMemo, useRef } from "react";
21
21
  import { Platform } from "react-native";
22
+ import { LicenseManager } from "@buoy-gg/license";
22
23
  import { reportOptionalRequireError } from "./optionalRequire.js";
23
24
  import { appSyncAdapter } from "./sync/appSyncAdapter.js";
24
25
  import { useDeviceIdentity } from "./deviceIdentity.js";
@@ -285,11 +286,11 @@ export function AutoExternalSync({
285
286
  useEffect(() => {
286
287
  if (!socket) return;
287
288
  const emitLicense = () => {
289
+ if (!LicenseManager.hasAccountAccess() || !socket.connected) return;
288
290
  socket.emit("device-license", {
289
291
  isPro: licenseRef.current.isPro ? "1" : "0",
290
292
  // Brokers older than the tier split ignore this; newer ones prefer it.
291
- tier: licenseRef.current.tier ?? (licenseRef.current.isPro ? "pro" : "anonymous"),
292
- licenseKey: licenseRef.current.licenseKey ?? ""
293
+ tier: licenseRef.current.tier ?? (licenseRef.current.isPro ? "pro" : "free")
293
294
  });
294
295
  };
295
296
  if (socket.connected) emitLicense();
@@ -11,7 +11,7 @@ import { getRankedToolIds, isDialUsageLoaded, loadDialUsage, recordToolUsage } f
11
11
  import { persistentStorage, useHintsDisabled, devToolsStorageKeys, buoyColors, ToolBackground, useToolBackground } from "@buoy-gg/shared-ui";
12
12
  import { DevToolsSettingsModal, useDevToolsSettings } from "../DevToolsSettingsModal";
13
13
  import { useProAccess } from "@buoy-gg/license";
14
- import { renderAppIcon } from "../AppIcon.js";
14
+ import { renderAppIcon } from "../AppIcon";
15
15
  import { useAppHost } from "../AppHost";
16
16
  import { OnboardingTooltip } from "./OnboardingTooltip";
17
17
  import { getDialLayout, MAX_DIAL_SLOTS, DIAL_BUTTON_SIZE, dialAnimationConfig, dialColors } from "@buoy-gg/floating-tools-core";
@@ -21,6 +21,14 @@ import { getDialLayout, MAX_DIAL_SLOTS, DIAL_BUTTON_SIZE, dialAnimationConfig, d
21
21
  // snapshot goes stale when the window resizes after load (web/desktop).
22
22
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
23
23
  const BUTTON_SIZE = DIAL_BUTTON_SIZE;
24
+ /** The dial face is a circle, so its centre is the middle of the measured square. */
25
+ const DIAL_BACKGROUND_FOCUS = {
26
+ x: 0.5,
27
+ y: 0.5
28
+ };
29
+ // Radius kept clear of grid spokes around the glass hub (its outer disc is
30
+ // 0.75 × BUTTON_SIZE, plus a breath).
31
+ const HUB_CLEARANCE = BUTTON_SIZE * 0.75 + 6;
24
32
 
25
33
  /**
26
34
  * The centre button's idle pulse, off by design.
@@ -97,6 +105,11 @@ export const DialDevTools = ({
97
105
  },
98
106
  gridLine: {
99
107
  width: circleSize
108
+ },
109
+ // Each spoke is two segments that stop short of the glass hub, so the
110
+ // six lines never meet in the middle where the background shows through.
111
+ spoke: {
112
+ width: circleSize / 2 - HUB_CLEARANCE
100
113
  }
101
114
  }), [circleSize]);
102
115
 
@@ -222,7 +235,7 @@ export const DialDevTools = ({
222
235
  iconComponent: typeof app.icon === "function" ? app.icon : undefined,
223
236
  color: app.color ?? buoyColors.primary,
224
237
  onPress: () => {
225
- // Record usage so frequently/recently used tools rank toward page 1.
238
+ // Put this tool first the next time the dial opens.
226
239
  void recordToolUsage(app.id);
227
240
 
228
241
  // Call the app's onPress callback if provided, passing actions for
@@ -586,16 +599,23 @@ export const DialDevTools = ({
586
599
  style: styles.gridPattern,
587
600
  children: Array.from({
588
601
  length: 6
589
- }).map((_, i) => /*#__PURE__*/_jsx(View, {
602
+ }).map((_, i) => /*#__PURE__*/_jsxs(View, {
590
603
  style: [styles.gridLine, sizeStyles.gridLine, {
591
604
  transform: [{
592
605
  rotate: `${i * 60}deg`
593
606
  }]
594
- }]
607
+ }],
608
+ children: [/*#__PURE__*/_jsx(View, {
609
+ style: [styles.spoke, sizeStyles.spoke, styles.spokeLeft]
610
+ }), /*#__PURE__*/_jsx(View, {
611
+ style: [styles.spoke, sizeStyles.spoke, styles.spokeRight]
612
+ })]
595
613
  }, i))
596
614
  }), backgroundId === "hyperspace" ? /*#__PURE__*/_jsx(DialHyperspace, {
597
615
  size: circleSize
598
- }) : /*#__PURE__*/_jsx(ToolBackground, {})]
616
+ }) : /*#__PURE__*/_jsx(ToolBackground, {
617
+ focus: DIAL_BACKGROUND_FOCUS
618
+ })]
599
619
  }), allDialIcons.filter(({
600
620
  page
601
621
  }) => page === safePage).map(({
@@ -753,9 +773,20 @@ const styles = StyleSheet.create({
753
773
  },
754
774
  gridLine: {
755
775
  position: "absolute",
776
+ height: 1
777
+ },
778
+ spoke: {
779
+ position: "absolute",
780
+ top: 0,
756
781
  height: 1,
757
782
  backgroundColor: dialColors.dialGridLine
758
783
  },
784
+ spokeLeft: {
785
+ left: 0
786
+ },
787
+ spokeRight: {
788
+ right: 0
789
+ },
759
790
  buttonContainer: {
760
791
  zIndex: 1,
761
792
  backgroundColor: "transparent",
@@ -766,6 +797,10 @@ const styles = StyleSheet.create({
766
797
  height: BUTTON_SIZE * 1.5,
767
798
  borderRadius: BUTTON_SIZE
768
799
  },
800
+ // The hub is glass: no rings, no opaque fill. The outer disc is a barely-
801
+ // there shade and the inner button a soft puck, so the tool background runs
802
+ // through the middle of the dial while BUOY / PRO stay legible. The spokes
803
+ // stop at HUB_CLEARANCE so nothing draws under it.
769
804
  buttonGradient: {
770
805
  width: "100%",
771
806
  height: "100%",
@@ -773,7 +808,7 @@ const styles = StyleSheet.create({
773
808
  alignItems: "center",
774
809
  justifyContent: "center",
775
810
  padding: 4,
776
- backgroundColor: dialColors.dialBackground,
811
+ backgroundColor: dialColors.hubHalo,
777
812
  position: "relative",
778
813
  overflow: "hidden"
779
814
  },
@@ -800,14 +835,12 @@ const styles = StyleSheet.create({
800
835
  borderRadius: BUTTON_SIZE
801
836
  },
802
837
  buttonBorder: {
803
- backgroundColor: dialColors.dialGridLine,
838
+ backgroundColor: dialColors.hubFill,
804
839
  alignItems: "center",
805
840
  justifyContent: "center",
806
841
  width: BUTTON_SIZE * 1.2,
807
842
  height: BUTTON_SIZE * 1.2,
808
- borderRadius: BUTTON_SIZE * 0.6,
809
- borderWidth: 2,
810
- borderColor: dialColors.dialBorder
843
+ borderRadius: BUTTON_SIZE * 0.6
811
844
  },
812
845
  button: {
813
846
  width: BUTTON_SIZE,
@@ -548,6 +548,8 @@ export function DialMenu({
548
548
  // Computed values
549
549
  const buttonContainerSize = layout.buttonSize * dialStyles.centerButton.containerRatio;
550
550
  const buttonBorderSize = layout.buttonSize * dialStyles.centerButton.borderRatio;
551
+ // Grid spokes stop this far from the centre: the hub's outer disc plus a breath.
552
+ const hubClearance = layout.buttonSize * dialStyles.centerButton.containerRatio / 2 + 6;
551
553
  const isAnimating = entranceComplete && !isExiting;
552
554
  return /*#__PURE__*/_jsxs("div", {
553
555
  role: "dialog",
@@ -623,10 +625,19 @@ export function DialMenu({
623
625
  left: 0,
624
626
  right: 0,
625
627
  height: dialStyles.grid.lineHeight,
626
- backgroundColor: dialColors.dialGridLine,
627
628
  transform: `rotate(${deg}deg)`,
628
629
  transformOrigin: 'center'
629
- }
630
+ },
631
+ children: ['left', 'right'].map(side => /*#__PURE__*/_jsx("div", {
632
+ style: {
633
+ position: 'absolute',
634
+ top: 0,
635
+ [side]: 0,
636
+ width: `calc(50% - ${hubClearance}px)`,
637
+ height: dialStyles.grid.lineHeight,
638
+ backgroundColor: dialColors.dialGridLine
639
+ }
640
+ }, side))
630
641
  }, deg)), paddedIcons.map((icon, index) => /*#__PURE__*/_jsx(DialIconItem, {
631
642
  icon: icon,
632
643
  index: index,
@@ -646,7 +657,10 @@ export function DialMenu({
646
657
  display: 'flex',
647
658
  alignItems: 'center',
648
659
  justifyContent: 'center',
649
- backgroundColor: dialColors.dialBackground,
660
+ // Glass hub — see the native twin: a barely-there shade on the outer
661
+ // disc, a soft puck inside, no rings, so the tool background runs
662
+ // through the middle of the dial.
663
+ backgroundColor: dialColors.hubHalo,
650
664
  borderRadius: layout.buttonSize,
651
665
  overflow: 'hidden',
652
666
  transform: `scale(${centerButtonScale * centerPulseScale})`,
@@ -685,8 +699,7 @@ export function DialMenu({
685
699
  width: buttonBorderSize,
686
700
  height: buttonBorderSize,
687
701
  borderRadius: layout.buttonSize * dialStyles.centerButton.borderRadiusRatio,
688
- backgroundColor: dialColors.dialGridLine,
689
- border: `${dialStyles.centerButton.borderWidth}px solid ${dialColors.dialBorder}`,
702
+ backgroundColor: dialColors.hubFill,
690
703
  display: 'flex',
691
704
  alignItems: 'center',
692
705
  justifyContent: 'center'
@@ -1,18 +1,12 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * Dial Usage Store - persists and ranks dial tool usage.
5
- *
6
- * Wraps the pure scoring logic from `@buoy-gg/floating-tools-core` with a
7
- * persisted, in-memory cache. The dial menu uses this to order tools by how
8
- * recently/frequently they are used, so the most-used tools land on page 1.
9
- *
10
- * The cache is loaded eagerly on import so `getRankedToolIds` can run
11
- * synchronously by the time the dial opens.
4
+ * Persists dial usage and orders tools by their last launch.
5
+ * The cache loads on import so opening the dial can rank tools synchronously.
12
6
  */
13
7
 
14
8
  import { persistentStorage, devToolsStorageKeys } from "@buoy-gg/shared-ui";
15
- import { rankToolIds, recordUsage, pruneUsage } from "@buoy-gg/floating-tools-core";
9
+ import { recordUsage, pruneUsage } from "@buoy-gg/floating-tools-core";
16
10
  const STORAGE_KEY = devToolsStorageKeys.dial.usage();
17
11
  let cache = {};
18
12
  let loaded = false;
@@ -50,15 +44,15 @@ export function isDialUsageLoaded() {
50
44
  return loaded;
51
45
  }
52
46
 
53
- /**
54
- * Rank tool ids by recency-weighted usage, highest first. Synchronous —
55
- * operates against the in-memory cache. Never-used tools keep their original
56
- * order as a tie-breaker.
57
- *
58
- * @param orderedIds - Tool ids in their default/registration order
59
- */
47
+ /** Most recently opened first; unused tools keep registration order. */
60
48
  export function getRankedToolIds(orderedIds) {
61
- return rankToolIds(orderedIds, cache, Date.now());
49
+ return orderedIds.map((id, index) => ({
50
+ id,
51
+ index,
52
+ lastUsed: cache[id]?.lastUsed ?? 0
53
+ })).sort((a, b) => b.lastUsed - a.lastUsed || a.index - b.index).map(({
54
+ id
55
+ }) => id);
62
56
  }
63
57
 
64
58
  /**
@@ -69,7 +63,9 @@ export function getRankedToolIds(orderedIds) {
69
63
  export async function recordToolUsage(id) {
70
64
  if (!id) return;
71
65
  if (!loaded) await loadDialUsage();
72
- const now = Date.now();
66
+
67
+ // Preserve launch order even when presses share a timestamp or the clock moves back.
68
+ const now = Math.max(Date.now(), ...Object.values(cache).map(entry => entry.lastUsed + 1));
73
69
  cache = pruneUsage(recordUsage(cache, id, now), now);
74
70
  try {
75
71
  await persistentStorage.setItem(STORAGE_KEY, JSON.stringify(cache));
@@ -1,64 +1,3 @@
1
1
  "use strict";
2
2
 
3
- import { Dimensions } from "react-native";
4
-
5
- /**
6
- * Shared constants for onboarding tooltip positioning
7
- * These ensure the floating menu aligns perfectly under the tooltip arrow
8
- * All positions are calculated dynamically based on screen size
9
- */
10
-
11
- // Arrow dimensions (relative positioning)
12
- export const ARROW_BOTTOM_OFFSET = -40; // Arrow extends below tooltip
13
- export const ARROW_HEIGHT = 20; // borderTopWidth of arrow
14
- export const SPACING_GAP = 10; // Gap between arrow tip and target element
15
-
16
- /**
17
- * Calculate the center Y position of the screen
18
- * This is used to vertically center the onboarding UI
19
- *
20
- * @returns Y coordinate at the center of the screen
21
- */
22
- export function getScreenCenterY() {
23
- const {
24
- height: screenHeight
25
- } = Dimensions.get("window");
26
- return screenHeight / 2;
27
- }
28
-
29
- /**
30
- * Calculate the tooltip position to center it vertically on screen
31
- * Uses flexbox-style centering - tooltip will center itself with its actual size
32
- *
33
- * @returns Distance from screen bottom for tooltip positioning
34
- */
35
- export function calculateTooltipPosition() {
36
- // Position from center - tooltip will be centered with transform or flex
37
- // We return a value that's slightly above center to account for the arrow below
38
- const centerY = getScreenCenterY();
39
-
40
- // Approximate offset to center the tooltip+arrow combo
41
- // This is a reasonable estimate that works for most tooltip sizes
42
- const estimatedHalfHeight = 130; // ~half of (tooltip + arrow)
43
-
44
- return centerY - estimatedHalfHeight;
45
- }
46
-
47
- /**
48
- * Calculate the position where a target element should be placed
49
- * to align directly under the tooltip arrow
50
- *
51
- * @returns Distance from screen bottom where element should be positioned
52
- */
53
- export function calculateTargetPosition() {
54
- const tooltipBottom = calculateTooltipPosition();
55
-
56
- // Arrow tip position = tooltip bottom + arrow offset (arrow extends below, so negative)
57
- const arrowTipFromBottom = tooltipBottom + ARROW_BOTTOM_OFFSET;
58
-
59
- // Subtract arrow height to get to the actual tip
60
- const actualArrowTip = arrowTipFromBottom - ARROW_HEIGHT;
61
-
62
- // Subtract spacing gap to position element below arrow
63
- return actualArrowTip - SPACING_GAP;
64
- }
3
+ export { ARROW_BOTTOM_OFFSET, ARROW_HEIGHT, SPACING_GAP, getScreenCenterY, calculateTooltipPosition, calculateTargetPosition } from "@buoy-gg/shared-ui";
@@ -1,27 +1,3 @@
1
1
  "use strict";
2
2
 
3
- /**
4
- * Web twin of onboardingConstants.ts — same math, window.innerHeight instead
5
- * of RN Dimensions.
6
- */
7
- export const ARROW_BOTTOM_OFFSET = -40; // Arrow extends below tooltip
8
- export const ARROW_HEIGHT = 20; // borderTopWidth of arrow
9
- export const SPACING_GAP = 10; // Gap between arrow tip and target element
10
-
11
- export function getScreenCenterY() {
12
- return window.innerHeight / 2;
13
- }
14
-
15
- /** Distance from screen bottom for tooltip positioning (centered-ish). */
16
- export function calculateTooltipPosition() {
17
- const estimatedHalfHeight = 130; // ~half of (tooltip + arrow)
18
- return getScreenCenterY() - estimatedHalfHeight;
19
- }
20
-
21
- /** Distance from screen bottom where the bubble should sit (under the arrow). */
22
- export function calculateTargetPosition() {
23
- const tooltipBottom = calculateTooltipPosition();
24
- const arrowTipFromBottom = tooltipBottom + ARROW_BOTTOM_OFFSET;
25
- const actualArrowTip = arrowTipFromBottom - ARROW_HEIGHT;
26
- return actualArrowTip - SPACING_GAP;
27
- }
3
+ export { ARROW_BOTTOM_OFFSET, ARROW_HEIGHT, SPACING_GAP, getScreenCenterY, calculateTooltipPosition, calculateTargetPosition } from "@buoy-gg/shared-ui/floating";