@buoy-gg/core 7.0.14 → 7.0.16

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 (99) hide show
  1. package/lib/commonjs/floatingMenu/AppHost.web.js +359 -0
  2. package/lib/commonjs/floatingMenu/AppHostLogic.js +6 -0
  3. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.web.js +787 -418
  4. package/lib/commonjs/floatingMenu/FloatingDevTools.js +1 -1
  5. package/lib/commonjs/floatingMenu/FloatingDevTools.web.js +244 -36
  6. package/lib/commonjs/floatingMenu/FloatingMenu.js +2 -2
  7. package/lib/commonjs/floatingMenu/MinimizedToolsContext.js +2 -3
  8. package/lib/commonjs/floatingMenu/MinimizedToolsStack.web.js +287 -0
  9. package/lib/commonjs/floatingMenu/autoDiscoverPresets.js +16 -0
  10. package/lib/commonjs/floatingMenu/autoExternalSync.js +9 -0
  11. package/lib/commonjs/floatingMenu/dial/DialDevTools.js +7 -4
  12. package/lib/commonjs/floatingMenu/dial/DialDevTools.web.js +373 -231
  13. package/lib/commonjs/floatingMenu/dial/DialIcon.js +25 -3
  14. package/lib/commonjs/floatingMenu/dial/OnboardingTooltip.js +1 -1
  15. package/lib/commonjs/floatingMenu/dial/OnboardingTooltip.web.js +154 -0
  16. package/lib/commonjs/floatingMenu/dial/onboardingConstants.web.js +34 -0
  17. package/lib/commonjs/floatingMenu/floatingTools.js +3 -3
  18. package/lib/commonjs/floatingMenu/floatingTools.web.js +191 -28
  19. package/lib/commonjs/index.js +2 -2
  20. package/lib/commonjs/index.web.js +19 -0
  21. package/lib/commonjs/utils/autoDiscoverPresets.web.js +50 -5
  22. package/lib/module/floatingMenu/AppHost.web.js +351 -0
  23. package/lib/module/floatingMenu/AppHostLogic.js +6 -0
  24. package/lib/module/floatingMenu/DevToolsSettingsModal.web.js +792 -422
  25. package/lib/module/floatingMenu/FloatingDevTools.js +2 -2
  26. package/lib/module/floatingMenu/FloatingDevTools.web.js +247 -38
  27. package/lib/module/floatingMenu/FloatingMenu.js +2 -2
  28. package/lib/module/floatingMenu/MinimizedToolsContext.js +2 -3
  29. package/lib/module/floatingMenu/MinimizedToolsStack.web.js +284 -0
  30. package/lib/module/floatingMenu/autoDiscoverPresets.js +16 -0
  31. package/lib/module/floatingMenu/autoExternalSync.js +9 -0
  32. package/lib/module/floatingMenu/dial/DialDevTools.js +8 -5
  33. package/lib/module/floatingMenu/dial/DialDevTools.web.js +375 -233
  34. package/lib/module/floatingMenu/dial/DialIcon.js +26 -4
  35. package/lib/module/floatingMenu/dial/OnboardingTooltip.js +1 -1
  36. package/lib/module/floatingMenu/dial/OnboardingTooltip.web.js +149 -0
  37. package/lib/module/floatingMenu/dial/onboardingConstants.web.js +27 -0
  38. package/lib/module/floatingMenu/floatingTools.js +3 -3
  39. package/lib/module/floatingMenu/floatingTools.web.js +190 -28
  40. package/lib/module/index.js +2 -2
  41. package/lib/module/index.web.js +5 -0
  42. package/lib/module/utils/autoDiscoverPresets.web.js +53 -7
  43. package/lib/typescript/commonjs/floatingMenu/AppHost.web.d.ts +38 -0
  44. package/lib/typescript/commonjs/floatingMenu/AppHost.web.d.ts.map +1 -0
  45. package/lib/typescript/commonjs/floatingMenu/AppHostLogic.d.ts +21 -1
  46. package/lib/typescript/commonjs/floatingMenu/AppHostLogic.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.web.d.ts +5 -2
  48. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -1
  49. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.web.d.ts +2 -14
  50. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsContext.d.ts +2 -20
  52. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsContext.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsStack.web.d.ts +21 -0
  54. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsStack.web.d.ts.map +1 -0
  55. package/lib/typescript/commonjs/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.web.d.ts +16 -2
  59. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/floatingMenu/dial/DialIcon.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/floatingMenu/dial/OnboardingTooltip.web.d.ts +9 -0
  62. package/lib/typescript/commonjs/floatingMenu/dial/OnboardingTooltip.web.d.ts.map +1 -0
  63. package/lib/typescript/commonjs/floatingMenu/dial/onboardingConstants.web.d.ts +13 -0
  64. package/lib/typescript/commonjs/floatingMenu/dial/onboardingConstants.web.d.ts.map +1 -0
  65. package/lib/typescript/commonjs/floatingMenu/floatingTools.web.d.ts +29 -2
  66. package/lib/typescript/commonjs/floatingMenu/floatingTools.web.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/index.web.d.ts +1 -0
  68. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/utils/autoDiscoverPresets.web.d.ts +2 -0
  70. package/lib/typescript/commonjs/utils/autoDiscoverPresets.web.d.ts.map +1 -1
  71. package/lib/typescript/module/floatingMenu/AppHost.web.d.ts +38 -0
  72. package/lib/typescript/module/floatingMenu/AppHost.web.d.ts.map +1 -0
  73. package/lib/typescript/module/floatingMenu/AppHostLogic.d.ts +21 -1
  74. package/lib/typescript/module/floatingMenu/AppHostLogic.d.ts.map +1 -1
  75. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.web.d.ts +5 -2
  76. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -1
  77. package/lib/typescript/module/floatingMenu/FloatingDevTools.web.d.ts +2 -14
  78. package/lib/typescript/module/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
  79. package/lib/typescript/module/floatingMenu/MinimizedToolsContext.d.ts +2 -20
  80. package/lib/typescript/module/floatingMenu/MinimizedToolsContext.d.ts.map +1 -1
  81. package/lib/typescript/module/floatingMenu/MinimizedToolsStack.web.d.ts +21 -0
  82. package/lib/typescript/module/floatingMenu/MinimizedToolsStack.web.d.ts.map +1 -0
  83. package/lib/typescript/module/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
  84. package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts.map +1 -1
  85. package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  86. package/lib/typescript/module/floatingMenu/dial/DialDevTools.web.d.ts +16 -2
  87. package/lib/typescript/module/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -1
  88. package/lib/typescript/module/floatingMenu/dial/DialIcon.d.ts.map +1 -1
  89. package/lib/typescript/module/floatingMenu/dial/OnboardingTooltip.web.d.ts +9 -0
  90. package/lib/typescript/module/floatingMenu/dial/OnboardingTooltip.web.d.ts.map +1 -0
  91. package/lib/typescript/module/floatingMenu/dial/onboardingConstants.web.d.ts +13 -0
  92. package/lib/typescript/module/floatingMenu/dial/onboardingConstants.web.d.ts.map +1 -0
  93. package/lib/typescript/module/floatingMenu/floatingTools.web.d.ts +29 -2
  94. package/lib/typescript/module/floatingMenu/floatingTools.web.d.ts.map +1 -1
  95. package/lib/typescript/module/index.web.d.ts +1 -0
  96. package/lib/typescript/module/index.web.d.ts.map +1 -1
  97. package/lib/typescript/module/utils/autoDiscoverPresets.web.d.ts +2 -0
  98. package/lib/typescript/module/utils/autoDiscoverPresets.web.d.ts.map +1 -1
  99. package/package.json +5 -5
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useMemo, useRef } from "react";
4
4
  import { StyleSheet, Pressable, View, Text, useWindowDimensions, Animated } from "react-native";
5
- import { getDialLayout, getIconPosition, getIconStaggerInputRange, DIAL_START_ANGLE, DIAL_ICON_SIZE, dialColors, dialStyles, dialAnimationConfig, floatingToolsColors } from "@buoy-gg/floating-tools-core";
5
+ import { getDialLayout, getIconPosition, getIconStaggerInputRange, DIAL_START_ANGLE, DIAL_ICON_SIZE, DIAL_ICON_GLYPH_SIZE, dialColors, dialStyles, dialAnimationConfig, floatingToolsColors } from "@buoy-gg/floating-tools-core";
6
6
 
7
7
  // The circle radius depends on the live window width and is computed inside
8
8
  // the component (must match DialDevTools' circle, which does the same).
@@ -166,7 +166,10 @@ export const DialIcon = ({
166
166
  style: styles.pressable,
167
167
  children: [/*#__PURE__*/_jsx(View, {
168
168
  style: styles.iconWrapper,
169
- children: icon.icon
169
+ children: /*#__PURE__*/_jsx(View, {
170
+ style: styles.iconClamp,
171
+ children: icon.icon
172
+ })
170
173
  }), /*#__PURE__*/_jsx(Text, {
171
174
  style: styles.label,
172
175
  numberOfLines: 1,
@@ -199,9 +202,12 @@ export const DialIcon = ({
199
202
  const IconComp = icon.iconComponent;
200
203
  return /*#__PURE__*/_jsx(IconComp, {
201
204
  slot: "dial",
202
- size: 32
205
+ size: DIAL_ICON_GLYPH_SIZE
203
206
  });
204
- })() : icon.icon
207
+ })() : /*#__PURE__*/_jsx(View, {
208
+ style: styles.iconClamp,
209
+ children: icon.icon
210
+ })
205
211
  }), /*#__PURE__*/_jsx(Text, {
206
212
  style: styles.label,
207
213
  numberOfLines: 1,
@@ -249,6 +255,22 @@ const styles = StyleSheet.create({
249
255
  alignItems: "center",
250
256
  justifyContent: "center"
251
257
  },
258
+ /**
259
+ * Hard clamp for PRE-RENDERED icon nodes (icon.icon) — the path where the
260
+ * glyph's size is baked in by whoever created the node (unavailable-tool
261
+ * registry entries, consumer custom tools) rather than controlled here. The
262
+ * fixed overflow-hidden box guarantees an oversized node can never blow past
263
+ * its slot and dwarf neighboring icons — worst case it center-crops. The
264
+ * iconComponent path stays unclipped: we render it at DIAL_ICON_GLYPH_SIZE
265
+ * ourselves, and clipping would shave off the icons' glow shadows.
266
+ */
267
+ iconClamp: {
268
+ width: DIAL_ICON_GLYPH_SIZE,
269
+ height: DIAL_ICON_GLYPH_SIZE,
270
+ alignItems: "center",
271
+ justifyContent: "center",
272
+ overflow: "hidden"
273
+ },
252
274
  label: {
253
275
  fontSize: dialStyles.icon.label.fontSize,
254
276
  fontWeight: dialStyles.icon.label.fontWeight,
@@ -4,7 +4,7 @@ import { useEffect, useRef } from "react";
4
4
  import { Animated, Pressable, StyleSheet, Text, View, Easing } from "react-native";
5
5
  import { absoluteFill } from "@buoy-gg/shared-ui";
6
6
  import { dialColors, buoyColors } from "@buoy-gg/shared-ui";
7
- import { calculateTooltipPosition, ARROW_BOTTOM_OFFSET, ARROW_HEIGHT } from "./onboardingConstants.js";
7
+ import { calculateTooltipPosition, ARROW_BOTTOM_OFFSET, ARROW_HEIGHT } from "./onboardingConstants";
8
8
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  export const OnboardingTooltip = ({
10
10
  visible,
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * OnboardingTooltip — Web twin of OnboardingTooltip.tsx.
5
+ * Centered glowing card with a down-pointing arrow, pulse loop, bouncing
6
+ * arrow, and a "Got it!" button. Rendered over a dark backdrop by callers.
7
+ * All values annotated with the native tokens they mirror.
8
+ */
9
+ import { useEffect, useState } from 'react';
10
+ import { dialColors, withAlpha } from '@buoy-gg/floating-tools-react';
11
+ import { ARROW_BOTTOM_OFFSET, ARROW_HEIGHT, calculateTooltipPosition } from "./onboardingConstants.web.js";
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ let onboardingKeyframesInjected = false;
14
+ function injectOnboardingKeyframes() {
15
+ if (onboardingKeyframesInjected || typeof document === 'undefined') return;
16
+ const style = document.createElement('style');
17
+ style.textContent = `
18
+ @keyframes buoyOnboardPulse {
19
+ 0%, 100% { transform: scale(1); }
20
+ 50% { transform: scale(1.02); }
21
+ }
22
+ @keyframes buoyOnboardArrowBounce {
23
+ 0%, 100% { transform: translateY(0); }
24
+ 50% { transform: translateY(-8px); }
25
+ }
26
+ `;
27
+ document.head.appendChild(style);
28
+ onboardingKeyframesInjected = true;
29
+ }
30
+
31
+ // native dialColors.dialShadow === buoyColors.primary (#20C997 family)
32
+ const PRIMARY = dialColors.dialShadow;
33
+ export function OnboardingTooltip({
34
+ title,
35
+ message,
36
+ onDismiss,
37
+ showArrow = true
38
+ }) {
39
+ const [entered, setEntered] = useState(false);
40
+ const [buttonPressed, setButtonPressed] = useState(false);
41
+ useEffect(() => {
42
+ injectOnboardingKeyframes();
43
+ const raf = requestAnimationFrame(() => setEntered(true));
44
+ return () => cancelAnimationFrame(raf);
45
+ }, []);
46
+ const monoLabel = {
47
+ fontFamily: 'monospace',
48
+ textTransform: 'uppercase'
49
+ };
50
+ return /*#__PURE__*/_jsx("div", {
51
+ style: {
52
+ position: 'fixed',
53
+ left: '50%',
54
+ bottom: calculateTooltipPosition(),
55
+ transform: `translateX(-50%) scale(${entered ? 1 : 0.8})`,
56
+ opacity: entered ? 1 : 0,
57
+ // native: fade 300ms + spring scale .8→1
58
+ transition: 'transform 300ms cubic-bezier(0.34, 1.3, 0.64, 1), opacity 300ms ease',
59
+ zIndex: 10002,
60
+ pointerEvents: 'auto'
61
+ },
62
+ children: /*#__PURE__*/_jsxs("div", {
63
+ style: {
64
+ position: 'relative',
65
+ animation: entered ? 'buoyOnboardPulse 3s ease-in-out infinite' : 'none'
66
+ },
67
+ children: [/*#__PURE__*/_jsx("div", {
68
+ style: {
69
+ position: 'absolute',
70
+ inset: -4,
71
+ borderRadius: 18,
72
+ backgroundColor: '#FFFFFF',
73
+ opacity: 0.15
74
+ }
75
+ }), /*#__PURE__*/_jsxs("div", {
76
+ style: {
77
+ position: 'relative',
78
+ maxWidth: 280,
79
+ backgroundColor: dialColors.dialBackground,
80
+ borderRadius: 16,
81
+ border: `2px solid ${dialColors.dialBorder}`,
82
+ padding: 20,
83
+ boxShadow: `0 0 20px ${withAlpha(PRIMARY, 0.6)}`,
84
+ display: 'flex',
85
+ flexDirection: 'column',
86
+ alignItems: 'center',
87
+ gap: 8
88
+ },
89
+ children: [/*#__PURE__*/_jsx("span", {
90
+ style: {
91
+ ...monoLabel,
92
+ fontSize: 16,
93
+ fontWeight: 900,
94
+ letterSpacing: 1,
95
+ color: PRIMARY,
96
+ textShadow: `0 0 4px ${PRIMARY}`,
97
+ textAlign: 'center'
98
+ },
99
+ children: title
100
+ }), /*#__PURE__*/_jsx("span", {
101
+ style: {
102
+ fontFamily: 'monospace',
103
+ fontSize: 13,
104
+ lineHeight: '20px',
105
+ color: '#FFFFFF',
106
+ textAlign: 'center'
107
+ },
108
+ children: message
109
+ }), /*#__PURE__*/_jsx("button", {
110
+ onClick: onDismiss,
111
+ onMouseDown: () => setButtonPressed(true),
112
+ onMouseUp: () => setButtonPressed(false),
113
+ onMouseLeave: () => setButtonPressed(false),
114
+ style: {
115
+ ...monoLabel,
116
+ marginTop: 8,
117
+ borderRadius: 8,
118
+ border: '2px solid #FFFFFF',
119
+ backgroundColor: 'rgba(255,255,255,0.1)',
120
+ padding: '10px 24px',
121
+ fontSize: 14,
122
+ fontWeight: 900,
123
+ letterSpacing: 1.5,
124
+ color: '#FFFFFF',
125
+ cursor: 'pointer',
126
+ opacity: buttonPressed ? 0.7 : 1,
127
+ transform: buttonPressed ? 'scale(0.96)' : 'scale(1)',
128
+ transition: 'transform 100ms ease, opacity 100ms ease'
129
+ },
130
+ children: "Got it!"
131
+ })]
132
+ }), showArrow && /*#__PURE__*/_jsx("div", {
133
+ style: {
134
+ position: 'absolute',
135
+ left: '50%',
136
+ bottom: ARROW_BOTTOM_OFFSET,
137
+ marginLeft: -12,
138
+ width: 0,
139
+ height: 0,
140
+ borderLeft: '12px solid transparent',
141
+ borderRight: '12px solid transparent',
142
+ borderTop: `${ARROW_HEIGHT}px solid ${PRIMARY}`,
143
+ filter: `drop-shadow(0 0 10px ${withAlpha(PRIMARY, 0.8)})`,
144
+ animation: entered ? 'buoyOnboardArrowBounce 1.6s ease-in-out infinite' : 'none'
145
+ }
146
+ })]
147
+ })
148
+ });
149
+ }
@@ -0,0 +1,27 @@
1
+ "use strict";
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,10 +3,10 @@
3
3
  import { useEffect, useMemo, useRef, useState, useContext, createContext, useCallback, Children } from "react";
4
4
  import { Animated, Dimensions, View, Text, TouchableOpacity } from "react-native";
5
5
  import { DraggableHeader, EnvironmentSelectorInline, GripVerticalIcon, buoyColors, getSafeAreaInsets, persistentStorage, useSafeAreaInsets } from "@buoy-gg/shared-ui";
6
- import { calculateTargetPosition } from "./dial/onboardingConstants.js";
7
- import { MinimizedToolsStack } from "./MinimizedToolsStack.js";
6
+ import { calculateTargetPosition } from "./dial/onboardingConstants";
7
+ import { MinimizedToolsStack } from "./MinimizedToolsStack";
8
8
  import { useMinimizedTools } from "./MinimizedToolsContext.js";
9
- import { useAppHost } from "./AppHost.js";
9
+ import { useAppHost } from "./AppHost";
10
10
 
11
11
  // Shared utilities from floating-tools-core
12
12
  import { getUserStatusConfig as getStatusConfig, filterValidChildren, floatingToolsColors } from "@buoy-gg/floating-tools-core";
@@ -4,7 +4,7 @@
4
4
  * FloatingTools - Web renderer using shared React bindings.
5
5
  *
6
6
  * This file contains ONLY web-specific code (React DOM):
7
- * - Mouse event handling
7
+ * - Pointer event handling (mouse + touch — WKWebView/Capacitor included)
8
8
  * - requestAnimationFrame animation
9
9
  * - Web JSX (div, span)
10
10
  * - localStorage adapter
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import { useState, useRef, useCallback, useEffect, useMemo, Children } from 'react';
18
+ import { calculateTargetPosition } from "./dial/onboardingConstants.web.js";
18
19
  import {
19
20
  // Hook and context
20
21
  useFloatingTools, FloatingToolsProvider, useFloatingToolsContext, interleaveWithDividers,
@@ -33,12 +34,23 @@ const webStorageAdapter = {
33
34
  setItem: (key, value) => localStorage.setItem(key, value)
34
35
  };
35
36
 
37
+ // =============================
38
+ // Native color values not present in floatingToolsColors
39
+ // (core's `secondary` is #E0E0E0 = buoyColors.text; the native bubble uses
40
+ // buoyColors.textSecondary #A0A0A0 for grip dots and "User" label text.
41
+ // Inlined here rather than changed in core because dial.ts also consumes
42
+ // floatingToolsColors.secondary for other surfaces.)
43
+ // =============================
44
+ // native shared-ui gameUIColors.ts: buoyColors.textSecondary = "#A0A0A0"
45
+ const NATIVE_TEXT_SECONDARY = '#A0A0A0';
46
+
36
47
  // =============================
37
48
  // GripVerticalIcon (web JSX)
38
49
  // =============================
39
50
  function GripVerticalIcon({
40
51
  size = 24,
41
- color = withAlpha(floatingToolsColors.secondary, 'CC')
52
+ // native shared-ui GripVerticalIcon.tsx: buoyColors.textSecondary + "CC"
53
+ color = withAlpha(NATIVE_TEXT_SECONDARY, 'CC')
42
54
  }) {
43
55
  const layout = getGripIconLayout(size);
44
56
  return /*#__PURE__*/_jsx("div", {
@@ -56,7 +68,9 @@ function GripVerticalIcon({
56
68
  flexDirection: 'column',
57
69
  alignItems: 'center',
58
70
  justifyContent: 'center',
59
- marginLeft: col === 0 ? 0 : layout.columnGap
71
+ // native GripVerticalIcon.tsx: marginHorizontal columnGap / 2
72
+ marginLeft: layout.columnGap / 2,
73
+ marginRight: layout.columnGap / 2
60
74
  },
61
75
  children: [0, 1, 2].map(row => /*#__PURE__*/_jsx("div", {
62
76
  style: {
@@ -64,7 +78,9 @@ function GripVerticalIcon({
64
78
  height: layout.dotSize,
65
79
  borderRadius: layout.dotSize / 2,
66
80
  backgroundColor: color,
67
- marginTop: row === 0 ? 0 : layout.rowGap
81
+ // native GripVerticalIcon.tsx: marginVertical rowGap / 2
82
+ marginTop: layout.rowGap / 2,
83
+ marginBottom: layout.rowGap / 2
68
84
  }
69
85
  }, row))
70
86
  }, col))
@@ -88,6 +104,16 @@ export function Divider() {
88
104
  // =============================
89
105
  // UserStatus (web JSX)
90
106
  // =============================
107
+ // Mirrors the native gameUIColorsCompat mapping in floatingTools.tsx:
108
+ // success/optional/muted come from the shared palette, but `secondary`
109
+ // (the "User" label text) is buoyColors.textSecondary, not core's secondary.
110
+ const userStatusColors = {
111
+ success: floatingToolsColors.success,
112
+ optional: floatingToolsColors.optional,
113
+ muted: floatingToolsColors.muted,
114
+ // native floatingTools.tsx: gameUIColorsCompat.secondary = buoyColors.textSecondary
115
+ secondary: NATIVE_TEXT_SECONDARY
116
+ };
91
117
  export function UserStatus({
92
118
  userRole,
93
119
  onPress
@@ -95,7 +121,7 @@ export function UserStatus({
95
121
  const {
96
122
  isDragging
97
123
  } = useFloatingToolsContext();
98
- const config = getUserStatusConfig(userRole, floatingToolsColors);
124
+ const config = getUserStatusConfig(userRole, userStatusColors);
99
125
  const handleClick = e => {
100
126
  if (!isDragging && onPress) {
101
127
  e.stopPropagation();
@@ -117,7 +143,8 @@ export function UserStatus({
117
143
  flexShrink: 0,
118
144
  cursor: onPress && !isDragging ? 'pointer' : 'default',
119
145
  background: 'none',
120
- border: 'none'
146
+ border: 'none',
147
+ margin: 0 // neutralize UA button margin (native View/Touchable has none)
121
148
  },
122
149
  children: [/*#__PURE__*/_jsx("div", {
123
150
  style: {
@@ -132,7 +159,11 @@ export function UserStatus({
132
159
  fontSize: 10,
133
160
  fontWeight: 500,
134
161
  color: config.textColor,
135
- letterSpacing: 0.3
162
+ letterSpacing: 0.3,
163
+ // native RN <Text> renders in the platform system font; buttons
164
+ // otherwise use the UA font on web
165
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
166
+ lineHeight: 'normal'
136
167
  },
137
168
  children: config.label
138
169
  })]
@@ -185,9 +216,20 @@ function useWebAnimation() {
185
216
  // Main FloatingTools Component
186
217
  // =============================
187
218
 
219
+ // Last known bubble position — the AppHost minimize animation targets this so
220
+ // tools shrink toward the stack instead of the old bottom-right dock.
221
+ let lastBubbleAnchor = null;
222
+ export function getBubbleAnchorPosition() {
223
+ return lastBubbleAnchor;
224
+ }
188
225
  export function FloatingTools({
189
226
  enablePositionPersistence = true,
190
- children
227
+ children,
228
+ pushToSide = false,
229
+ suspendPointerEvents = false,
230
+ centerOnboarding = false,
231
+ minimizedStack,
232
+ hasMinimizedStack = false
191
233
  }) {
192
234
  const bubbleRef = useRef(null);
193
235
 
@@ -219,6 +261,15 @@ export function FloatingTools({
219
261
  }
220
262
  }, [isInitialized, state.position, setAnimatedPosition]);
221
263
 
264
+ // Publish the bubble anchor for the AppHost minimize-target (stack lives
265
+ // fused to the bubble now, not in a fixed dock).
266
+ useEffect(() => {
267
+ lastBubbleAnchor = {
268
+ x: animatedPosition.x,
269
+ y: animatedPosition.y
270
+ };
271
+ }, [animatedPosition]);
272
+
222
273
  // Handle window resize
223
274
  useEffect(() => {
224
275
  const handleResize = () => {
@@ -243,17 +294,34 @@ export function FloatingTools({
243
294
  }
244
295
  }, [bubbleMeasured, actions]);
245
296
 
246
- // Web-specific: mouse event handling
247
- const [isMouseDown, setIsMouseDown] = useState(false);
248
- const handleMouseDown = useCallback(e => {
297
+ // Keep the bubble below notches/status bars (WKWebView with viewport-fit=cover).
298
+ useEffect(() => {
299
+ const probe = document.createElement('div');
300
+ probe.style.cssText = 'position:fixed;top:env(safe-area-inset-top);left:env(safe-area-inset-left);' + 'visibility:hidden;pointer-events:none';
301
+ document.body.appendChild(probe);
302
+ const rect = probe.getBoundingClientRect();
303
+ document.body.removeChild(probe);
304
+ if (rect.top > 0 || rect.left > 0) {
305
+ actions.setMinPosition({
306
+ x: Math.max(rect.left, 10),
307
+ y: Math.max(rect.top, 10)
308
+ });
309
+ }
310
+ }, [actions]);
311
+
312
+ // Web-specific: pointer event handling (mouse + touch).
313
+ // preventDefault on pointerdown suppresses the compatibility mouse events,
314
+ // and touchAction: 'none' on the bubble stops touch-drags from scrolling.
315
+ const [isPointerDown, setIsPointerDown] = useState(false);
316
+ const handlePointerDown = useCallback(e => {
249
317
  e.preventDefault();
250
- setIsMouseDown(true);
318
+ setIsPointerDown(true);
251
319
  actions.handleDragStart({
252
320
  clientX: e.clientX,
253
321
  clientY: e.clientY
254
322
  });
255
323
  }, [actions]);
256
- const handleMouseMove = useCallback(e => {
324
+ const handlePointerMove = useCallback(e => {
257
325
  const result = actions.handleDragMove({
258
326
  clientX: e.clientX,
259
327
  clientY: e.clientY
@@ -262,8 +330,8 @@ export function FloatingTools({
262
330
  setAnimatedPosition(result.position);
263
331
  }
264
332
  }, [actions, setAnimatedPosition]);
265
- const handleMouseUp = useCallback(e => {
266
- setIsMouseDown(false);
333
+ const handlePointerUp = useCallback(e => {
334
+ setIsPointerDown(false);
267
335
  const result = actions.handleDragEnd({
268
336
  clientX: e.clientX,
269
337
  clientY: e.clientY
@@ -280,18 +348,88 @@ export function FloatingTools({
280
348
  }
281
349
  }, [actions, animateTo, state.position]);
282
350
  useEffect(() => {
283
- if (isMouseDown) {
284
- window.addEventListener('mousemove', handleMouseMove);
285
- window.addEventListener('mouseup', handleMouseUp);
351
+ if (isPointerDown) {
352
+ window.addEventListener('pointermove', handlePointerMove);
353
+ window.addEventListener('pointerup', handlePointerUp);
354
+ window.addEventListener('pointercancel', handlePointerUp);
286
355
  return () => {
287
- window.removeEventListener('mousemove', handleMouseMove);
288
- window.removeEventListener('mouseup', handleMouseUp);
356
+ window.removeEventListener('pointermove', handlePointerMove);
357
+ window.removeEventListener('pointerup', handlePointerUp);
358
+ window.removeEventListener('pointercancel', handlePointerUp);
289
359
  };
290
360
  }
291
- }, [isMouseDown, handleMouseMove, handleMouseUp]);
361
+ }, [isPointerDown, handlePointerMove, handlePointerUp]);
362
+
363
+ // Live refs so the pushToSide effect never animates from a stale closure
364
+ // (the from-position jump read as a "pop" when restoring).
365
+ const statePositionRef = useRef(state.position);
366
+ statePositionRef.current = state.position;
367
+ const isHiddenRef = useRef(state.isHidden);
368
+ isHiddenRef.current = state.isHidden;
369
+
370
+ // pushToSide: forceHide to the edge while a dial/tool is open; forceShow
371
+ // restores the saved position when it closes (200ms, native parity).
372
+ // Native semantics ported exactly (floatingTools.tsx:537-603):
373
+ // - push only when the bubble isn't already hidden (a user-hidden bubble
374
+ // is left alone, and we don't claim it as OUR push)
375
+ // - restore only when the bubble is STILL hidden from OUR push — if the
376
+ // user tapped/dragged it back out while a tool was open
377
+ // (userWantsVisibleRef), it stays exactly where they put it
378
+ const pushedRef = useRef(false);
379
+ useEffect(() => {
380
+ if (!isInitialized) return;
381
+ if (pushToSide && !pushedRef.current) {
382
+ if (isHiddenRef.current) return; // user hid it themselves — not our push
383
+ pushedRef.current = true;
384
+ const {
385
+ targetPosition
386
+ } = actions.forceHide();
387
+ animateTo(statePositionRef.current, targetPosition, () => {});
388
+ } else if (!pushToSide && pushedRef.current) {
389
+ pushedRef.current = false;
390
+ // User already showed it manually while the tool was open — leave it.
391
+ if (!isHiddenRef.current) return;
392
+ const {
393
+ targetPosition
394
+ } = actions.forceShow();
395
+ // The bubble may have grown while hidden (row tools/env badge toggled
396
+ // on) — clamp the restored position so it stays fully on-screen
397
+ // (native handles this via its width-change auto-reposition).
398
+ const width = bubbleRef.current?.getBoundingClientRect().width ?? 100;
399
+ const clamped = {
400
+ x: Math.max(10, Math.min(targetPosition.x, window.innerWidth - width - 10)),
401
+ y: targetPosition.y
402
+ };
403
+ animateTo(statePositionRef.current, clamped, () => {
404
+ actions.commitPosition(clamped);
405
+ });
406
+ }
407
+ // eslint-disable-next-line react-hooks/exhaustive-deps
408
+ }, [pushToSide, isInitialized]);
409
+
410
+ // centerOnboarding: park the bubble under the tooltip arrow while the
411
+ // welcome tooltip is up (native calculateTargetPosition math).
412
+ useEffect(() => {
413
+ if (!centerOnboarding || !isInitialized) return;
414
+ const bubbleWidth = bubbleRef.current?.getBoundingClientRect().width ?? 100;
415
+ const bubbleHeight = bubbleRef.current?.getBoundingClientRect().height ?? 32;
416
+ const fromBottom = calculateTargetPosition();
417
+ const target = {
418
+ x: (window.innerWidth - bubbleWidth) / 2,
419
+ y: window.innerHeight - fromBottom - bubbleHeight
420
+ };
421
+ animateTo(statePositionRef.current, target, () => {});
422
+ // eslint-disable-next-line react-hooks/exhaustive-deps
423
+ }, [centerOnboarding, isInitialized]);
292
424
 
293
425
  // Interleave children with dividers
294
426
  const actionsContent = useMemo(() => interleaveWithDividers(Children.toArray(children), key => /*#__PURE__*/_jsx(Divider, {}, key)), [children]);
427
+
428
+ // Suppress the first paint until the persisted position has loaded so the
429
+ // bubble doesn't flash at 0,0 (native floatingTools.tsx:935-937).
430
+ if (!isInitialized) {
431
+ return null;
432
+ }
295
433
  return /*#__PURE__*/_jsxs("div", {
296
434
  ref: bubbleRef,
297
435
  role: "toolbar",
@@ -305,21 +443,43 @@ export function FloatingTools({
305
443
  flexDirection: 'row',
306
444
  alignItems: 'center',
307
445
  backgroundColor: floatingToolsColors.panel,
308
- borderRadius: 6,
446
+ // native: top-left corner + top border flatten when the minimized
447
+ // stack is fused above (floatingTools.tsx containerStyle)
448
+ borderTopLeftRadius: hasMinimizedStack ? 0 : 6,
449
+ borderTopRightRadius: 6,
450
+ borderBottomLeftRadius: 6,
451
+ borderBottomRightRadius: 6,
309
452
  borderWidth: state.isDragging ? 2 : 1,
453
+ borderTopWidth: state.isDragging ? 2 : hasMinimizedStack ? 0 : 1,
310
454
  borderStyle: 'solid',
311
455
  borderColor: state.isDragging ? floatingToolsColors.dragActive : withAlpha(floatingToolsColors.muted, '66'),
312
- boxShadow: state.isDragging ? `0 6px 12px ${withAlpha(floatingToolsColors.dragActive, '99')}` : '0 4px 8px rgba(0, 0, 0, 0.3)',
456
+ // native floatingTools.tsx: shadowOffset {0, 6|4}, shadowRadius 12|8,
457
+ // shadowOpacity 0.6|0.3, shadowColor dragActive+"99"|"#000".
458
+ // Dragging color = 0x99 (0.6) alpha x 0.6 shadowOpacity = ~0.36 effective.
459
+ boxShadow: state.isDragging ? `0 6px 12px ${withAlpha(floatingToolsColors.dragActive, 0.36)}` : '0 4px 8px rgba(0, 0, 0, 0.3)',
313
460
  userSelect: 'none',
314
- touchAction: 'none'
461
+ touchAction: 'none',
462
+ pointerEvents: suspendPointerEvents ? 'none' : 'auto'
315
463
  },
316
- children: [/*#__PURE__*/_jsx("div", {
464
+ children: [minimizedStack && /*#__PURE__*/_jsx("div", {
465
+ style: {
466
+ position: 'absolute',
467
+ bottom: '100%',
468
+ left: -1,
469
+ // align with the bubble border edge
470
+ width: VISIBLE_HANDLE_WIDTH,
471
+ overflow: 'visible',
472
+ zIndex: 1001
473
+ },
474
+ children: minimizedStack
475
+ }), /*#__PURE__*/_jsx("div", {
317
476
  role: "slider",
318
477
  "aria-label": "Drag handle",
319
478
  "aria-valuetext": `Position: ${Math.round(animatedPosition.x)}, ${Math.round(animatedPosition.y)}`,
320
479
  tabIndex: 0,
321
- onMouseDown: handleMouseDown,
480
+ onPointerDown: handlePointerDown,
322
481
  style: {
482
+ touchAction: 'none',
323
483
  paddingLeft: 6,
324
484
  paddingRight: 6,
325
485
  paddingTop: 6,
@@ -329,9 +489,11 @@ export function FloatingTools({
329
489
  alignItems: 'center',
330
490
  justifyContent: 'center',
331
491
  width: VISIBLE_HANDLE_WIDTH,
492
+ boxSizing: 'border-box',
493
+ // RN width includes padding/border
332
494
  borderRight: `1px solid ${withAlpha(floatingToolsColors.muted, '66')}`,
333
- borderTopLeftRadius: 5,
334
- borderBottomLeftRadius: 5,
495
+ // native dragHandleStyle sets NO corner radius (container overflow is
496
+ // visible); the 10%-alpha bg square corner matches native rendering
335
497
  cursor: state.isDragging ? 'grabbing' : 'grab'
336
498
  },
337
499
  children: /*#__PURE__*/_jsx(GripVerticalIcon, {
@@ -18,7 +18,7 @@ export { FloatingMenu } from "./floatingMenu/FloatingMenu.js";
18
18
  export * from "./floatingMenu/types.js";
19
19
  export { DevToolsSettingsModal, useDevToolsSettings } from "./floatingMenu/DevToolsSettingsModal";
20
20
  // Export AppHost components for advanced use cases
21
- export { AppHostProvider, AppOverlay, useAppHost } from "./floatingMenu/AppHost.js";
21
+ export { AppHostProvider, AppOverlay, useAppHost } from "./floatingMenu/AppHost";
22
22
 
23
23
  // Export DevTools visibility tracking
24
24
  export { DevToolsVisibilityProvider, useDevToolsVisibility } from "./floatingMenu/DevToolsVisibilityContext.js";
@@ -28,7 +28,7 @@ export { toggleStateManager } from "./floatingMenu/ToggleStateManager.js";
28
28
 
29
29
  // Export minimized tools components
30
30
  export { MinimizedToolsProvider, useMinimizedTools, getIconPosition, getIconSize } from "./floatingMenu/MinimizedToolsContext.js";
31
- export { MinimizedToolsStack } from "./floatingMenu/MinimizedToolsStack.js";
31
+ export { MinimizedToolsStack } from "./floatingMenu/MinimizedToolsStack";
32
32
 
33
33
  // Re-export license functionality from @buoy-gg/license
34
34
  // These are re-exported for convenience so users can import from @buoy-gg/core
@@ -15,6 +15,11 @@ export { FloatingDevTools } from "./floatingMenu/FloatingDevTools.web.js";
15
15
  export { FloatingTools, UserStatus, Divider } from "./floatingMenu/floatingTools.web.js";
16
16
  export { DialMenu } from "./floatingMenu/dial/DialDevTools.web.js";
17
17
  export { SettingsModal } from "./floatingMenu/DevToolsSettingsModal.web.js";
18
+ // Tool host (open tool panels + minimized dock) — web twin of the native
19
+ // AppHostProvider/AppOverlay/useAppHost. FloatingDevTools wires these up
20
+ // automatically; exported for advanced/standalone use.
21
+ export { AppHostProvider, AppOverlay, useAppHost } from "./floatingMenu/AppHost.web.js";
22
+
18
23
  // Export auto-discovery utilities (web version)
19
24
  export { autoDiscoverWebPresets, autoDiscoverWithCustom, getToolsBySlot, getEnabledTools, defaultToolPresets } from "./utils/autoDiscoverPresets.web.js";
20
25