@buoy-gg/shared-ui 7.0.28 → 7.0.29

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 (117) hide show
  1. package/lib/commonjs/JsModal.js +12 -2
  2. package/lib/commonjs/dataViewer/DataTreeActionDock.js +1 -0
  3. package/lib/commonjs/dataViewer/DataViewer.js +6 -3
  4. package/lib/commonjs/dataViewer/VirtualizedDataExplorer.js +13 -4
  5. package/lib/commonjs/hooks/safe-area-impl.js +1 -1
  6. package/lib/commonjs/license/FeatureGate.js +9 -0
  7. package/lib/commonjs/ui/backgrounds/registry.js +3 -2
  8. package/lib/commonjs/ui/backgrounds/variants/meteors.js +222 -78
  9. package/lib/commonjs/ui/components/Badge.js +3 -1
  10. package/lib/commonjs/ui/components/CompactRow.js +10 -1
  11. package/lib/commonjs/ui/components/CopyButton.js +3 -1
  12. package/lib/commonjs/ui/components/DraggableHeader.js +5 -1
  13. package/lib/commonjs/ui/components/DynamicFilterView.js +23 -4
  14. package/lib/commonjs/ui/components/EventStepperFooter.js +8 -0
  15. package/lib/commonjs/ui/components/FilterComponents.js +18 -2
  16. package/lib/commonjs/ui/components/ModalHeader.js +12 -3
  17. package/lib/commonjs/ui/components/PowerToggleButton.js +2 -0
  18. package/lib/commonjs/ui/components/SearchBar.js +6 -1
  19. package/lib/commonjs/ui/components/SectionHeader.js +3 -1
  20. package/lib/commonjs/ui/components/TabSelector.js +15 -4
  21. package/lib/commonjs/ui/components/WindowControls.js +68 -96
  22. package/lib/commonjs/ui/gameUI/constants/gameUIColors.js +12 -8
  23. package/lib/module/JsModal.js +12 -2
  24. package/lib/module/dataViewer/DataTreeActionDock.js +1 -0
  25. package/lib/module/dataViewer/DataViewer.js +6 -3
  26. package/lib/module/dataViewer/VirtualizedDataExplorer.js +13 -4
  27. package/lib/module/hooks/safe-area-impl.js +1 -1
  28. package/lib/module/license/FeatureGate.js +9 -0
  29. package/lib/module/ui/backgrounds/registry.js +3 -2
  30. package/lib/module/ui/backgrounds/variants/meteors.js +224 -80
  31. package/lib/module/ui/components/Badge.js +3 -1
  32. package/lib/module/ui/components/CompactRow.js +10 -1
  33. package/lib/module/ui/components/CopyButton.js +3 -1
  34. package/lib/module/ui/components/DraggableHeader.js +5 -1
  35. package/lib/module/ui/components/DynamicFilterView.js +23 -4
  36. package/lib/module/ui/components/EventStepperFooter.js +8 -0
  37. package/lib/module/ui/components/FilterComponents.js +18 -2
  38. package/lib/module/ui/components/ModalHeader.js +12 -3
  39. package/lib/module/ui/components/PowerToggleButton.js +2 -0
  40. package/lib/module/ui/components/SearchBar.js +6 -1
  41. package/lib/module/ui/components/SectionHeader.js +3 -1
  42. package/lib/module/ui/components/TabSelector.js +15 -4
  43. package/lib/module/ui/components/WindowControls.js +69 -97
  44. package/lib/module/ui/gameUI/constants/gameUIColors.js +12 -8
  45. package/lib/typescript/commonjs/JsModal.d.ts.map +1 -1
  46. package/lib/typescript/commonjs/dataViewer/DataTreeActionDock.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/dataViewer/DataViewer.d.ts +2 -0
  48. package/lib/typescript/commonjs/dataViewer/DataViewer.d.ts.map +1 -1
  49. package/lib/typescript/commonjs/dataViewer/VirtualizedDataExplorer.d.ts +9 -0
  50. package/lib/typescript/commonjs/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  52. package/lib/typescript/commonjs/license/FeatureGate.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/ui/backgrounds/registry.d.ts.map +1 -1
  54. package/lib/typescript/commonjs/ui/backgrounds/variants/meteors.d.ts +22 -6
  55. package/lib/typescript/commonjs/ui/backgrounds/variants/meteors.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/ui/components/Badge.d.ts +3 -1
  57. package/lib/typescript/commonjs/ui/components/Badge.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/ui/components/CompactRow.d.ts +5 -1
  59. package/lib/typescript/commonjs/ui/components/CompactRow.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/ui/components/CopyButton.d.ts +5 -0
  61. package/lib/typescript/commonjs/ui/components/CopyButton.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/ui/components/DraggableHeader.d.ts +2 -0
  63. package/lib/typescript/commonjs/ui/components/DraggableHeader.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/ui/components/DynamicFilterView.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/ui/components/EventStepperFooter.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/ui/components/FilterComponents.d.ts +10 -2
  67. package/lib/typescript/commonjs/ui/components/FilterComponents.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/ui/components/ModalHeader.d.ts +16 -2
  69. package/lib/typescript/commonjs/ui/components/ModalHeader.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/ui/components/PowerToggleButton.d.ts +4 -1
  71. package/lib/typescript/commonjs/ui/components/PowerToggleButton.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/ui/components/SearchBar.d.ts +7 -1
  73. package/lib/typescript/commonjs/ui/components/SearchBar.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/ui/components/SectionHeader.d.ts +7 -1
  75. package/lib/typescript/commonjs/ui/components/SectionHeader.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/ui/components/TabSelector.d.ts +20 -1
  77. package/lib/typescript/commonjs/ui/components/TabSelector.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/ui/components/WindowControls.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts +5 -0
  80. package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
  81. package/lib/typescript/module/JsModal.d.ts.map +1 -1
  82. package/lib/typescript/module/dataViewer/DataTreeActionDock.d.ts.map +1 -1
  83. package/lib/typescript/module/dataViewer/DataViewer.d.ts +2 -0
  84. package/lib/typescript/module/dataViewer/DataViewer.d.ts.map +1 -1
  85. package/lib/typescript/module/dataViewer/VirtualizedDataExplorer.d.ts +9 -0
  86. package/lib/typescript/module/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
  87. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  88. package/lib/typescript/module/license/FeatureGate.d.ts.map +1 -1
  89. package/lib/typescript/module/ui/backgrounds/registry.d.ts.map +1 -1
  90. package/lib/typescript/module/ui/backgrounds/variants/meteors.d.ts +22 -6
  91. package/lib/typescript/module/ui/backgrounds/variants/meteors.d.ts.map +1 -1
  92. package/lib/typescript/module/ui/components/Badge.d.ts +3 -1
  93. package/lib/typescript/module/ui/components/Badge.d.ts.map +1 -1
  94. package/lib/typescript/module/ui/components/CompactRow.d.ts +5 -1
  95. package/lib/typescript/module/ui/components/CompactRow.d.ts.map +1 -1
  96. package/lib/typescript/module/ui/components/CopyButton.d.ts +5 -0
  97. package/lib/typescript/module/ui/components/CopyButton.d.ts.map +1 -1
  98. package/lib/typescript/module/ui/components/DraggableHeader.d.ts +2 -0
  99. package/lib/typescript/module/ui/components/DraggableHeader.d.ts.map +1 -1
  100. package/lib/typescript/module/ui/components/DynamicFilterView.d.ts.map +1 -1
  101. package/lib/typescript/module/ui/components/EventStepperFooter.d.ts.map +1 -1
  102. package/lib/typescript/module/ui/components/FilterComponents.d.ts +10 -2
  103. package/lib/typescript/module/ui/components/FilterComponents.d.ts.map +1 -1
  104. package/lib/typescript/module/ui/components/ModalHeader.d.ts +16 -2
  105. package/lib/typescript/module/ui/components/ModalHeader.d.ts.map +1 -1
  106. package/lib/typescript/module/ui/components/PowerToggleButton.d.ts +4 -1
  107. package/lib/typescript/module/ui/components/PowerToggleButton.d.ts.map +1 -1
  108. package/lib/typescript/module/ui/components/SearchBar.d.ts +7 -1
  109. package/lib/typescript/module/ui/components/SearchBar.d.ts.map +1 -1
  110. package/lib/typescript/module/ui/components/SectionHeader.d.ts +7 -1
  111. package/lib/typescript/module/ui/components/SectionHeader.d.ts.map +1 -1
  112. package/lib/typescript/module/ui/components/TabSelector.d.ts +20 -1
  113. package/lib/typescript/module/ui/components/TabSelector.d.ts.map +1 -1
  114. package/lib/typescript/module/ui/components/WindowControls.d.ts.map +1 -1
  115. package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts +5 -0
  116. package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
  117. package/package.json +4 -4
@@ -107,6 +107,9 @@ function DynamicFilterView({
107
107
  const countIsZero = hasCount && option.count === 0;
108
108
  const showLabel = section.type !== "method" || option.icon;
109
109
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
110
+ testID: `buoy-filter-option-${option.id}`,
111
+ accessibilityRole: "button",
112
+ accessibilityLabel: String(option.label ?? option.id),
110
113
  style: [styles.filterCard, option.isActive && styles.activeFilterCard],
111
114
  onPress: () => onFilterChange?.(option.id, option.value),
112
115
  children: [option.icon ? /*#__PURE__*/(0, _jsxRuntime.jsx)(option.icon, {
@@ -191,7 +194,8 @@ function DynamicFilterView({
191
194
  children: !includeFilterManager.showAddInput ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_FilterComponents.AddFilterButton, {
192
195
  onPress: () => includeFilterManager.setShowAddInput(true),
193
196
  color: _gameUIColors.buoyColors.success,
194
- label: "Add include pattern"
197
+ label: "Add include pattern",
198
+ testID: "buoy-filter-include-add-open"
195
199
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_FilterComponents.AddFilterInput, {
196
200
  value: includeFilterManager.newFilter,
197
201
  onChange: includeFilterManager.setNewFilter,
@@ -201,9 +205,11 @@ function DynamicFilterView({
201
205
  includeFilterManager.setNewFilter("");
202
206
  },
203
207
  placeholder: includeOnlySection.placeholder || "Enter pattern to include...",
204
- color: _gameUIColors.buoyColors.success
208
+ color: _gameUIColors.buoyColors.success,
209
+ testID: "buoy-filter-include-input"
205
210
  })
206
211
  }), includeOnlySection.patterns.size > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
212
+ testID: "buoy-filter-include-scroll",
207
213
  style: styles.includeOnlyFiltersContent,
208
214
  showsVerticalScrollIndicator: true,
209
215
  nestedScrollEnabled: true,
@@ -248,7 +254,11 @@ function DynamicFilterView({
248
254
  color: _gameUIColors.buoyColors.primary
249
255
  })]
250
256
  })
251
- }), activePatterns.size > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
257
+ }), activePatterns.size > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView
258
+ // The active patterns live in their own short scroller, so a
259
+ // pattern added a moment ago is usually below its fold.
260
+ , {
261
+ testID: "buoy-filter-active-scroll",
252
262
  style: styles.excludeFiltersContent,
253
263
  showsVerticalScrollIndicator: true,
254
264
  nestedScrollEnabled: true,
@@ -287,6 +297,9 @@ function DynamicFilterView({
287
297
  },
288
298
  style: styles.availableKeyItem,
289
299
  "sentry-label": "ignore-touchable-opacity",
300
+ testID: `buoy-filter-available-${item}`,
301
+ accessibilityRole: "button",
302
+ accessibilityLabel: `Add filter ${item}`,
290
303
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
291
304
  style: styles.availableKeyText,
292
305
  numberOfLines: 1,
@@ -344,7 +357,13 @@ function DynamicFilterView({
344
357
  };
345
358
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
346
359
  style: styles.container,
347
- children: [renderTabs(), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
360
+ children: [renderTabs(), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView
361
+ // The filter screen's own scroller. Named because the sections it
362
+ // holds — including the active-pattern list — run past the bottom of
363
+ // a docked sheet, so reaching one means scrolling THIS, not the short
364
+ // nested scroller the pattern happens to sit in.
365
+ , {
366
+ testID: "buoy-filter-scroll",
348
367
  style: styles.content,
349
368
  contentContainerStyle: styles.scrollContent,
350
369
  showsVerticalScrollIndicator: false,
@@ -53,6 +53,9 @@ const EventStepperFooter = exports.EventStepperFooter = /*#__PURE__*/(0, _react.
53
53
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
54
54
  onPress: onPrevious,
55
55
  disabled: isFirstItem,
56
+ testID: "tool-stepper-previous",
57
+ accessibilityRole: "button",
58
+ accessibilityLabel: "Previous",
56
59
  style: [styles.navButton, isNight && styles.nightNavButton, isFirstItem && styles.navButtonDisabled],
57
60
  activeOpacity: 0.7,
58
61
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_lucideIcons.ChevronLeft, {
@@ -65,15 +68,20 @@ const EventStepperFooter = exports.EventStepperFooter = /*#__PURE__*/(0, _react.
65
68
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
66
69
  style: styles.counterContainer,
67
70
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
71
+ testID: "tool-stepper-counter",
68
72
  style: [styles.counterText, isNight && styles.nightCounterText],
69
73
  children: [itemLabel, " ", currentIndex + 1, " of ", totalItems]
70
74
  }), subtitle && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
75
+ testID: "tool-stepper-subtitle",
71
76
  style: [styles.subtitleText, isNight && styles.nightSubtitleText],
72
77
  children: subtitle
73
78
  })]
74
79
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
75
80
  onPress: onNext,
76
81
  disabled: isLastItem,
82
+ testID: "tool-stepper-next",
83
+ accessibilityRole: "button",
84
+ accessibilityLabel: "Next",
77
85
  style: [styles.navButton, isNight && styles.nightNavButton, isLastItem && styles.navButtonDisabled],
78
86
  activeOpacity: 0.7,
79
87
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
@@ -67,13 +67,16 @@ function AddFilterInput({
67
67
  onSubmit,
68
68
  onCancel,
69
69
  placeholder = "Add filter...",
70
- color = "#E5E7EB"
70
+ color = "#E5E7EB",
71
+ testID = "buoy-filter-input"
71
72
  }) {
72
73
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
73
74
  style: [styles.inputContainer, {
74
75
  borderColor: `${color}40`
75
76
  }],
76
77
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
78
+ testID: testID,
79
+ accessibilityLabel: placeholder,
77
80
  value: value,
78
81
  onChangeText: onChange,
79
82
  onSubmitEditing: onSubmit,
@@ -92,6 +95,9 @@ function AddFilterInput({
92
95
  style: styles.inputButtons,
93
96
  children: [value.trim() && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
94
97
  onPress: onSubmit,
98
+ testID: `${testID}-add`,
99
+ accessibilityLabel: "Add filter",
100
+ accessibilityRole: "button",
95
101
  style: [styles.inlineAddButton, {
96
102
  backgroundColor: `${color}15`,
97
103
  borderColor: `${color}40`
@@ -104,6 +110,9 @@ function AddFilterInput({
104
110
  })
105
111
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
106
112
  onPress: onCancel,
113
+ testID: `${testID}-cancel`,
114
+ accessibilityLabel: "Cancel adding filter",
115
+ accessibilityRole: "button",
107
116
  style: styles.cancelButton,
108
117
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.X, {
109
118
  size: 16,
@@ -119,13 +128,17 @@ function AddFilterInput({
119
128
  function AddFilterButton({
120
129
  onPress,
121
130
  color = "#E5E7EB",
122
- label = "Add Filter"
131
+ label = "Add Filter",
132
+ testID = "buoy-filter-add-open"
123
133
  }) {
124
134
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
125
135
  style: [styles.addButton, {
126
136
  borderColor: `${color}40`
127
137
  }],
128
138
  onPress: onPress,
139
+ testID: testID,
140
+ accessibilityLabel: label,
141
+ accessibilityRole: "button",
129
142
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_index.Plus, {
130
143
  size: 14,
131
144
  color: color
@@ -154,6 +167,9 @@ function FilterList({
154
167
  style: styles.filterItemRow,
155
168
  onPress: () => onRemoveFilter?.(filter),
156
169
  activeOpacity: 0.8,
170
+ testID: `buoy-filter-active-${filter}`,
171
+ accessibilityLabel: `Remove filter ${filter}`,
172
+ accessibilityRole: "button",
157
173
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
158
174
  style: [styles.filterItemText, {
159
175
  color
@@ -94,7 +94,8 @@ function Content({
94
94
  subtitle,
95
95
  children,
96
96
  centered,
97
- noMargin
97
+ noMargin,
98
+ testID
98
99
  }) {
99
100
  if (children) {
100
101
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
@@ -107,10 +108,12 @@ function Content({
107
108
  children: [title && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
108
109
  style: [styles.headerTitle, centered && styles.headerTitleCentered],
109
110
  numberOfLines: 1,
111
+ testID: testID,
110
112
  children: title
111
113
  }), subtitle && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
112
114
  style: [styles.headerSubtitle, centered && styles.headerSubtitleCentered],
113
115
  numberOfLines: 1,
116
+ testID: testID ? `${testID}-subtitle` : undefined,
114
117
  children: subtitle
115
118
  })]
116
119
  });
@@ -138,7 +141,8 @@ function HeaderSearch({
138
141
  onChangeText,
139
142
  onCancel,
140
143
  placeholder = "Search...",
141
- autoFocus = true
144
+ autoFocus = true,
145
+ testID
142
146
  }) {
143
147
  const inputRef = (0, _react.useRef)(null);
144
148
  (0, _react.useEffect)(() => {
@@ -157,10 +161,15 @@ function HeaderSearch({
157
161
  value: value,
158
162
  onChangeText: onChangeText,
159
163
  autoCorrect: false,
160
- autoCapitalize: "none"
164
+ autoCapitalize: "none",
165
+ testID: testID,
166
+ accessibilityLabel: placeholder
161
167
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
162
168
  onPress: onCancel,
163
169
  style: styles.searchCancelButton,
170
+ accessibilityRole: "button",
171
+ accessibilityLabel: "Clear search",
172
+ testID: testID ? `${testID}-clear` : undefined,
164
173
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.X, {
165
174
  size: 12,
166
175
  color: _nightTheme.night.color.textSecondary
@@ -32,6 +32,7 @@ function PowerToggleButton({
32
32
  onToggle,
33
33
  size = "medium",
34
34
  accessibilityLabel,
35
+ testID,
35
36
  disabled = false
36
37
  }) {
37
38
  const buttonSize = size === "small" ? 28 : 32;
@@ -40,6 +41,7 @@ function PowerToggleButton({
40
41
  onPress: onToggle,
41
42
  disabled: disabled,
42
43
  accessibilityLabel: accessibilityLabel,
44
+ testID: testID,
43
45
  accessibilityRole: "button",
44
46
  style: [styles.button, {
45
47
  width: buttonSize,
@@ -22,6 +22,7 @@ function SearchBar({
22
22
  containerStyle,
23
23
  autoFocus = false,
24
24
  onSubmitEditing,
25
+ testID,
25
26
  returnKeyType
26
27
  }) {
27
28
  const [isFocused, setIsFocused] = (0, _react.useState)(false);
@@ -79,10 +80,14 @@ function SearchBar({
79
80
  autoCapitalize: "none",
80
81
  autoCorrect: false,
81
82
  onSubmitEditing: onSubmitEditing,
82
- returnKeyType: returnKeyType
83
+ returnKeyType: returnKeyType,
84
+ testID: testID
83
85
  }), value.length > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
84
86
  onPress: handleClear,
85
87
  style: styles.clearButton,
88
+ testID: testID ? `${testID}-clear` : undefined,
89
+ accessibilityLabel: "Clear search",
90
+ accessibilityRole: "button",
86
91
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.X, {
87
92
  size: 14,
88
93
  color: _index2.gameUIColors.secondary
@@ -54,7 +54,8 @@ function Title({
54
54
  function Badge({
55
55
  count,
56
56
  color = _gameUIColors.buoyColors.primary,
57
- children
57
+ children,
58
+ testID
58
59
  }) {
59
60
  const backgroundColor = `${color}15`;
60
61
  const borderColor = `${color}33`;
@@ -64,6 +65,7 @@ function Badge({
64
65
  borderColor
65
66
  }],
66
67
  children: count !== undefined ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
68
+ testID: testID,
67
69
  style: [styles.badgeText, {
68
70
  color
69
71
  }],
@@ -19,24 +19,29 @@ var _jsxRuntime = require("react/jsx-runtime");
19
19
  function TabSelector({
20
20
  tabs,
21
21
  activeTab,
22
- onTabChange
22
+ onTabChange,
23
+ testIDPrefix
23
24
  }) {
24
25
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
25
26
  style: styles.container,
26
27
  children: tabs.map(tab => {
27
28
  const active = activeTab === tab.key;
29
+ const disabled = !!tab.disabled && !active;
28
30
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
29
31
  "sentry-label": "ignore user interaction",
32
+ testID: testIDPrefix ? `${testIDPrefix}-${tab.key}` : undefined,
30
33
  accessibilityRole: "tab",
31
34
  accessibilityState: {
32
- selected: active
35
+ selected: active,
36
+ disabled
33
37
  },
34
38
  accessibilityLabel: tab.label,
35
39
  accessibilityHint: `View ${tab.label.toLowerCase()}`,
40
+ disabled: disabled,
36
41
  onPress: () => onTabChange(tab.key),
37
- style: [styles.tabButton, active && styles.tabButtonActive],
42
+ style: [styles.tabButton, active && styles.tabButtonActive, disabled && styles.tabButtonDisabled],
38
43
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
39
- style: [styles.tabButtonText, active && styles.tabButtonTextActive],
44
+ style: [styles.tabButtonText, active && styles.tabButtonTextActive, disabled && styles.tabButtonTextDisabled],
40
45
  children: tab.label
41
46
  }), active ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
42
47
  style: styles.underline
@@ -73,6 +78,12 @@ const styles = _reactNative.StyleSheet.create({
73
78
  fontWeight: "600",
74
79
  color: _nightTheme.night.color.text
75
80
  },
81
+ tabButtonDisabled: {
82
+ opacity: 0.45
83
+ },
84
+ tabButtonTextDisabled: {
85
+ color: _nightTheme.night.color.textTertiary
86
+ },
76
87
  underline: {
77
88
  position: "absolute",
78
89
  bottom: 1.5,
@@ -112,6 +112,7 @@ function WindowControls({
112
112
  style: [styles.dot, styles.closeDot],
113
113
  activeOpacity: 0.8,
114
114
  accessibilityLabel: "Close modal",
115
+ testID: "tool-window-close",
115
116
  accessibilityRole: "button",
116
117
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.X, {
117
118
  size: ICON_SIZE,
@@ -140,10 +141,8 @@ function ExpandableWindowControls({
140
141
  }) {
141
142
  const isNight = variant === "night";
142
143
  const [expanded, setExpanded] = (0, _react.useState)(false);
143
- const [triggerLayout, setTriggerLayout] = (0, _react.useState)(null);
144
144
  const expandAnim = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current;
145
145
  const dismissTimeout = (0, _react.useRef)(null);
146
- const triggerRef = (0, _react.useRef)(null);
147
146
  const ToggleModeIcon = mode === "floating" ? _index.DockBottom : _index.FloatWindow;
148
147
  const toggleModeLabel = mode === "floating" ? "Dock to bottom sheet" : "Make floating window";
149
148
  const clearDismissTimer = (0, _react.useCallback)(() => {
@@ -152,38 +151,34 @@ function ExpandableWindowControls({
152
151
  dismissTimeout.current = null;
153
152
  }
154
153
  }, []);
154
+
155
+ // JS driver on purpose: native-driven animations on this node silently
156
+ // never run inside JsModal's subtree (the panel sat at opacity 0 forever,
157
+ // making the expanded controls unreachable). The popover is tiny and the
158
+ // animation is 150ms — the JS driver is fine.
155
159
  const collapse = (0, _react.useCallback)(() => {
156
160
  clearDismissTimer();
157
161
  _reactNative.Animated.spring(expandAnim, {
158
162
  toValue: 0,
159
163
  tension: 200,
160
164
  friction: 20,
161
- useNativeDriver: true
165
+ useNativeDriver: false
162
166
  }).start(() => {
163
167
  setExpanded(false);
164
168
  });
165
169
  }, [expandAnim, clearDismissTimer]);
166
170
  const expand = (0, _react.useCallback)(() => {
167
- // Measure trigger position on screen before expanding
168
- triggerRef.current?.measureInWindow((x, y, width, height) => {
169
- setTriggerLayout({
170
- x,
171
- y,
172
- width,
173
- height
174
- });
175
- setExpanded(true);
176
- _reactNative.Animated.spring(expandAnim, {
177
- toValue: 1,
178
- tension: 180,
179
- friction: 18,
180
- useNativeDriver: true
181
- }).start();
171
+ setExpanded(true);
172
+ _reactNative.Animated.spring(expandAnim, {
173
+ toValue: 1,
174
+ tension: 180,
175
+ friction: 18,
176
+ useNativeDriver: false
177
+ }).start();
182
178
 
183
- // Auto-dismiss
184
- clearDismissTimer();
185
- dismissTimeout.current = setTimeout(collapse, AUTO_DISMISS_MS);
186
- });
179
+ // Auto-dismiss
180
+ clearDismissTimer();
181
+ dismissTimeout.current = setTimeout(collapse, AUTO_DISMISS_MS);
187
182
  }, [expandAnim, collapse, clearDismissTimer]);
188
183
  (0, _react.useEffect)(() => {
189
184
  return clearDismissTimer;
@@ -202,17 +197,12 @@ function ExpandableWindowControls({
202
197
  inputRange: [0, 0.5, 1],
203
198
  outputRange: [0, 0.8, 1]
204
199
  });
205
-
206
- // Count visible buttons for layout calculation
207
- const buttonCount = 1 + (onMinimize ? 1 : 0) + (onToggleMode ? 1 : 0);
208
- const expandedWidth = buttonCount * EXPANDED_BUTTON_SIZE + (buttonCount - 1) * EXPANDED_BUTTON_SPACING + EXPANDED_PADDING * 2;
209
- const expandedHeight = EXPANDED_BUTTON_SIZE + EXPANDED_PADDING * 2;
210
200
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
211
201
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
212
- ref: triggerRef,
213
202
  onPress: expand,
214
203
  activeOpacity: 0.7,
215
204
  accessibilityLabel: "Open window controls",
205
+ testID: "tool-window-controls",
216
206
  accessibilityRole: "button",
217
207
  style: styles.trigger,
218
208
  hitSlop: {
@@ -246,71 +236,52 @@ function ExpandableWindowControls({
246
236
  })
247
237
  })]
248
238
  })
249
- }), expanded && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Modal, {
250
- transparent: true,
251
- visible: true,
252
- statusBarTranslucent: true,
253
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableWithoutFeedback, {
254
- onPress: collapse,
255
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
256
- style: styles.overlay,
257
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
258
- style: [styles.expandedPanel, {
259
- transform: [{
260
- scale
261
- }],
262
- opacity,
263
- // Position the expanded panel anchored to trigger's top-right
264
- top: triggerLayout ? triggerLayout.y - EXPANDED_PADDING : 0,
265
- right: triggerLayout ? Math.max(4,
266
- // screen width minus trigger's right edge
267
- // We don't need Dimensions here since we use measureInWindow
268
- // which gives absolute coords. right = screenWidth - (x + width)
269
- 0) : 0,
270
- // Position using left instead, anchored to trigger's right edge
271
- left: triggerLayout ? triggerLayout.x + triggerLayout.width - expandedWidth : undefined
272
- }],
273
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableWithoutFeedback, {
274
- children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
275
- style: [styles.expandedContainer, isNight && styles.nightExpandedContainer],
276
- children: [onMinimize && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
277
- onPress: () => handleAction(onMinimize),
278
- style: [styles.expandedButton, isNight ? styles.nightMinimizeButton : styles.minimizeButtonExpanded],
279
- activeOpacity: 0.7,
280
- accessibilityLabel: "Minimize modal",
281
- accessibilityRole: "button",
282
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.Minus, {
283
- size: EXPANDED_ICON_SIZE,
284
- color: isNight ? _nightTheme.night.color.warning : "#7A5A00",
285
- strokeWidth: 2
286
- })
287
- }), onToggleMode && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
288
- onPress: () => handleAction(onToggleMode),
289
- style: [styles.expandedButton, isNight ? styles.nightToggleButton : styles.expandButtonExpanded],
290
- activeOpacity: 0.7,
291
- accessibilityLabel: toggleModeLabel,
292
- accessibilityRole: "button",
293
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(ToggleModeIcon, {
294
- size: EXPANDED_ICON_SIZE,
295
- color: isNight ? _nightTheme.night.color.accent : "#004A1A",
296
- strokeWidth: 2
297
- })
298
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
299
- onPress: () => handleAction(onClose),
300
- style: [styles.expandedButton, isNight ? styles.nightCloseButton : styles.closeButtonExpanded],
301
- activeOpacity: 0.7,
302
- accessibilityLabel: "Close modal",
303
- accessibilityRole: "button",
304
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.X, {
305
- size: EXPANDED_ICON_SIZE,
306
- color: isNight ? _nightTheme.night.color.danger : "#4A0000",
307
- strokeWidth: 2
308
- })
309
- })]
310
- })
311
- })
239
+ }), expanded && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
240
+ style: [styles.expandedPanelInline, {
241
+ transform: [{
242
+ scale
243
+ }],
244
+ opacity
245
+ }],
246
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
247
+ style: [styles.expandedContainer, isNight && styles.nightExpandedContainer],
248
+ children: [onMinimize && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
249
+ onPress: () => handleAction(onMinimize),
250
+ style: [styles.expandedButton, isNight ? styles.nightMinimizeButton : styles.minimizeButtonExpanded],
251
+ activeOpacity: 0.7,
252
+ accessibilityLabel: "Minimize modal",
253
+ testID: "tool-window-minimize",
254
+ accessibilityRole: "button",
255
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.Minus, {
256
+ size: EXPANDED_ICON_SIZE,
257
+ color: isNight ? _nightTheme.night.color.warning : "#7A5A00",
258
+ strokeWidth: 2
312
259
  })
313
- })
260
+ }), onToggleMode && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
261
+ onPress: () => handleAction(onToggleMode),
262
+ style: [styles.expandedButton, isNight ? styles.nightToggleButton : styles.expandButtonExpanded],
263
+ activeOpacity: 0.7,
264
+ accessibilityLabel: toggleModeLabel,
265
+ testID: "tool-window-mode",
266
+ accessibilityRole: "button",
267
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(ToggleModeIcon, {
268
+ size: EXPANDED_ICON_SIZE,
269
+ color: isNight ? _nightTheme.night.color.accent : "#004A1A",
270
+ strokeWidth: 2
271
+ })
272
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
273
+ onPress: () => handleAction(onClose),
274
+ style: [styles.expandedButton, isNight ? styles.nightCloseButton : styles.closeButtonExpanded],
275
+ activeOpacity: 0.7,
276
+ accessibilityLabel: "Close modal",
277
+ testID: "tool-window-close",
278
+ accessibilityRole: "button",
279
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.X, {
280
+ size: EXPANDED_ICON_SIZE,
281
+ color: isNight ? _nightTheme.night.color.danger : "#4A0000",
282
+ strokeWidth: 2
283
+ })
284
+ })]
314
285
  })
315
286
  })]
316
287
  });
@@ -345,11 +316,12 @@ const styles = _reactNative.StyleSheet.create({
345
316
  expandDot: {
346
317
  backgroundColor: COLORS.toggleMode
347
318
  },
348
- overlay: {
349
- flex: 1
350
- },
351
- expandedPanel: {
319
+ // Anchored to the windowControlsContainer (absolute top-right of the
320
+ // modal chrome); grows leftward/downward over the header.
321
+ expandedPanelInline: {
352
322
  position: "absolute",
323
+ top: -EXPANDED_PADDING,
324
+ right: -EXPANDED_PADDING,
353
325
  zIndex: 9999
354
326
  },
355
327
  expandedContainer: {
@@ -206,13 +206,17 @@ const buoyColors = exports.buoyColors = {
206
206
  // Red
207
207
  info: "#00B8E6" // Cyan for info/devices
208
208
  };
209
- // Fixed dial colors for cyberpunk theme
209
+ /**
210
+ * LEGACY copy of the dial palette — the source of truth is `dialColors` in
211
+ * @buoy-gg/floating-tools-core (`src/dial.ts`); keep the two in sync.
212
+ * Values mirror the night theme (see `ui/night/nightTheme.ts`).
213
+ */
210
214
  const dialColors = exports.dialColors = {
211
- dialBackground: gameUIColors.pureBlack,
212
- dialGradient1: `${gameUIColors.info}10`,
213
- dialGradient2: `${gameUIColors.info}08`,
214
- dialGradient3: `${gameUIColors.info}15`,
215
- dialBorder: `${gameUIColors.info}40`,
216
- dialShadow: gameUIColors.info,
217
- dialGridLine: `${gameUIColors.info}26`
215
+ dialBackground: "#050505",
216
+ dialGradient1: "rgba(255, 255, 255, 0.05)",
217
+ dialGradient2: "rgba(255, 255, 255, 0.03)",
218
+ dialGradient3: "rgba(255, 255, 255, 0.06)",
219
+ dialBorder: "rgba(255, 255, 255, 0.12)",
220
+ dialShadow: "#000000",
221
+ dialGridLine: "rgba(255, 255, 255, 0.08)"
218
222
  };
@@ -494,6 +494,16 @@ const JsModalComponent = ({
494
494
  }
495
495
  }, []);
496
496
 
497
+ // Parents routinely pass an inline `onModeChange` (a new identity every
498
+ // render). The persisted-state load below is mount/key-scoped — if the
499
+ // callback identity re-triggered it, every parent re-render would reload
500
+ // the OLD persisted mode and stomp a live toggleMode() before the 500ms
501
+ // save debounce could write the new one (mode flipped to floating, then
502
+ // snapped back ~50ms later). So the callback rides in a ref instead of
503
+ // the dependency list.
504
+ const onModeChangeRef = useRef(onModeChange);
505
+ onModeChangeRef.current = onModeChange;
506
+
497
507
  // Load persisted state on mount
498
508
  useEffect(() => {
499
509
  if (!enablePersistence || !persistenceKey) {
@@ -508,7 +518,7 @@ const JsModalComponent = ({
508
518
  if (savedState.mode) {
509
519
  setMode(savedState.mode);
510
520
  // Notify parent of loaded mode
511
- onModeChange?.(savedState.mode);
521
+ onModeChangeRef.current?.(savedState.mode);
512
522
  }
513
523
 
514
524
  // Restore bottom sheet height
@@ -535,7 +545,7 @@ const JsModalComponent = ({
535
545
  return () => {
536
546
  mounted = false;
537
547
  };
538
- }, [persistenceKey, enablePersistence, onModeChange, animatedBottomPosition, animatedFloatingHeight, animatedWidth, floatingPosition]);
548
+ }, [persistenceKey, enablePersistence, animatedBottomPosition, animatedFloatingHeight, animatedWidth, floatingPosition]);
539
549
 
540
550
  // Cleanup on unmount
541
551
  useEffect(() => {
@@ -82,6 +82,7 @@ function Tile({
82
82
  const color = danger && enabled ? T.dangerText : T.tileIcon;
83
83
  return /*#__PURE__*/_jsxs(TouchableOpacity, {
84
84
  "sentry-label": "ignore devtools tree action",
85
+ testID: `buoy-tree-action-${id}`,
85
86
  accessibilityLabel: label,
86
87
  accessibilityState: {
87
88
  disabled: !enabled
@@ -27,7 +27,8 @@ export const DataViewer = ({
27
27
  onRequestInlineEdit,
28
28
  inlineEditPath,
29
29
  renderInlineEditor,
30
- selectPath
30
+ selectPath,
31
+ testIDPrefix
31
32
  }) => {
32
33
  const [activeFilter, setActiveFilter] = useState(null);
33
34
 
@@ -109,7 +110,8 @@ export const DataViewer = ({
109
110
  maxDepth: maxDepth,
110
111
  rawMode: rawMode,
111
112
  initialExpanded: initialExpanded,
112
- disableCopy: disableCopy
113
+ disableCopy: disableCopy,
114
+ testIDPrefix: testIDPrefix
113
115
  });
114
116
  }
115
117
 
@@ -125,7 +127,8 @@ export const DataViewer = ({
125
127
  onRequestInlineEdit: onRequestInlineEdit,
126
128
  inlineEditPath: inlineEditPath,
127
129
  renderInlineEditor: renderInlineEditor,
128
- selectPath: selectPath
130
+ selectPath: selectPath,
131
+ testIDPrefix: testIDPrefix
129
132
  });
130
133
  };
131
134
  return /*#__PURE__*/_jsxs(View, {